This topic is critical because a well-designed mapping prevents errors, saves a lot of time and has a big impact on performance.
In this section, we are going to present most of the Entities that we needed for the application. A couple of implementation techniques (from inheritance types to relationship cases) have been chosen here and highlighted for example purposes.
The How it works… section will explain why and how things are defined in the way they are and what were the thoughts that drove us toward the Entities' definitions we made.
The following steps will help you create Entities in the application:
edu.zipcloud.cloudstreetmarket.core.entities
. First, created three simple entities as shown here:User
entity:@Entity @Table(name="user") public class User implements Serializable{ private static final long serialVersionUID = 1990856213905768044L; @Id @Column(nullable = false) private String loginName; private String password; private String profileImg; @OneToMany(mappedBy="user", cascade = {CascadeType.ALL}, fetch = FetchType.LAZY) @OrderBy("id desc") private Set<Transaction> transactions = new LinkedHashSet< >(); ... }
Transaction
entity:@Entity @Table(name="transaction") public class Transaction implements Serializable{ private static final long serialVersionUID = -6433721069248439324L; @Id @GeneratedValue private int id; @ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "user_name") private User user; @Enumerated(EnumType.STRING) private Action type; @OneToOne(fetch = FetchType.EAGER) @JoinColumn(name = "stock_quote_id") private StockQuote quote; private int quantity; ... }
Market
entity:@Entity @Table(name="market") public class Market implements Serializable { private static final long serialVersionUID = -6433721069248439324L; @Id private String id; private String name; @OneToMany(mappedBy = "market", cascade = { CascadeType.ALL }, fetch = FetchType.EAGER) private Set<Index> indices = new LinkedHashSet<>(); ... }
Historic
entity:@Entity @Inheritance(strategy = InheritanceType.SINGLE_TABLE) @DiscriminatorColumn(name = "historic_type") @Table(name="historic") public abstract class Historic { private static final long serialVersionUID = -802306391915956578L; @Id @GeneratedValue private int id; private double open; private double high; private double low; private double close; private double volume; @Column(name="adj_close") private double adjClose; @Column(name="change_percent") private double changePercent; @Temporal(TemporalType.TIMESTAMP) @Column(name="from_date") private Date fromDate; @Temporal(TemporalType.TIMESTAMP) @Column(name="to_date") private Date toDate; @Enumerated(EnumType.STRING) @Column(name="interval") private QuotesInterval interval; ... }
We have also created the two Historic subtypes, HistoricalIndex
and HistoricalStock
:
@Entity @DiscriminatorValue("idx") public class HistoricalIndex extends Historic implements Serializable { private static final long serialVersionUID = -802306391915956578L; @ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "index_code") private Index index; ... } @Entity @DiscriminatorValue("stk") public class HistoricalStock extends Historic implements Serializable { private static final long serialVersionUID = -802306391915956578L; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "stock_code") private StockProduct stock; private double bid; private double ask; ... }
Product
entity with its StockProduct subtypes:@Entity @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) public abstract class Product { private static final long serialVersionUID = - 802306391915956578L; @Id private String code; private String name; ... } @Entity @Table(name="stock") public class StockProduct extends Product implements Serializable{ private static final long serialVersionUID = 1620238240796817290L; private String currency; @ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "market_id") private Market market; ... }
@Entity @Table(name="index_value") public class Index implements Serializable{ private static final long serialVersionUID = -2919348303931939346L; @Id private String code; private String name; @ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "market_id", nullable=true) private Market market; @ManyToMany(fetch = FetchType.LAZY) @JoinTable(name = "stock_indices", joinColumns={@JoinColumn(name = "index_code")}, inverseJoinColumns={@JoinColumn(name ="stock_code")}) private Set<StockProduct> stocks = new LinkedHashSet<>(); ... }
Quote
abstract entity with its two subtypes, StockQuote
and IndexQuote
, have created (indices are not products, but we can still get instant snapshots from them, and the Yahoo! financial data provider will later be called to get these instant quotes):@Entity @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) public abstract class Quote { @Id @GeneratedValue(strategy = GenerationType.TABLE) protected Integer id; private Date date; private double open; @Column(name = "previous_close") private double previousClose; private double last; ... } @Entity @Table(name="stock_quote") public class StockQuote extends Quote implements Serializable{ private static final long serialVersionUID = -8175317254623555447L; @ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "stock_code") private StockProduct stock; private double bid; private double ask; ... } @Entity @Table(name="index_quote") public class IndexQuote extends Quote implements Serializable{ private static final long serialVersionUID = -8175317254623555447L; @ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "index_code") private Index index; ... }
We are going to go through some basic and more advanced concepts that we have used to build our relational mapping.
An entity, to be considered as such by the API requires the following conditions:
@Entity
annotation.@Id
annotation on a specific entity field).Both databases and Java objects have specific concepts. The metadata annotations for Entities, along with the configuration by default, describe the relational mapping.
An entity class maps a table. Not specifying a @Table(name="xxx")
annotation on the Type level will map the entity class to the table named with the entity name (this is the default naming).
The @Table
annotation also has an optional schema
attribute, which allows us to bind the table to a schema in the SQL queries (for example public.user.ID
). This schema
attribute will override the default schema JPA property, which can be defined on the persistence unit.
As with the table names, the column name to map a field to is specified with the @Column(name="xxx")
annotation. Again, this annotation is optional, and not specifying it will make the mapping fall back to the default naming scheme, which is literally the cased name of the field (in case of single words, it is often a good option).
The fields of an entity class must not be defined as public. Also keep in mind that you can almost persist all the standard Java Types (primitive Types, wrappers, Strings, Bytes or Character arrays, and enumerated) and large numeric Types, such as BigDecimals
or BigIntegers
, but also JDBC temporal types (java.sql.Date
, java.sql.TimeStamp
) and even serializable objects.
The fields of an entity (if not tagged as @Transient
) correspond to the values that the database row will have for each column. A column mapping can also be defined from a getter (without necessarily having a corresponding field).
The @Id
annotation defines the entity identifier. Also, defining this @Id
annotation on a field or getter defines whether the table columns should be mapped by a field or on a by getters.
When using a getter access mode, and when a @Column
annotation is not specified, the default naming scheme for the column name uses the JavaBeans property naming standard (for example, the getUser()
getter would correspond to the user
column).
As we have seen already, the @Id
annotation defines the entity's identifier. A persistence context will always manage no more than one instance of an entity with a single identifier.
The @Id
annotation on an entity class must map the persistent identifier for a table, which is the primary key.
We have defined entity inheritance for subtypes of Products
, Historics
, and Quotes
. When two Entities are close enough to be grouped into a single concept, and if they actually can be associated with a parent entity in the application, it is worth using the JPA inheritance.
Depending upon the persistence strategy for specific data, different storage options can be considered for inheritance mapping.
The JPA allows us to configure an inheritance model from different strategies.
This strategy expects or creates one big table with a discriminator field on the schema. This table hosts the parent-entity fields; these are common to all subentities. It also hosts all the fields of subentity classes. Consequently, if an entity corresponds to one subtype or another, it will populate the specific fields and leave the others blank.
The following table represents the Historic
table with its HISTORIC_TYPE
discriminator:
This strategy uses specific tables for concrete Entities. There is no discriminator involved here, just specific tables for subtypes. These tables carry both common and specific fields.
We have, for example, implemented this strategy for the Quote
entity and its concrete StockQuote
and IndexQuote
Entities:
Entities have the capability to reflect database foreign keys and table to table relationships in their class attributes.
On the application side, because these relationships are built transparently by the entity managers, a huge amount of developments are bypassed.
Before talking about relationships between entities, it is necessary to understand what we plan to do in the cloudstreet-market application.
As introduced in Chapter 1, Setup Routine for an Enterprise Spring Application, we will pull financial data from providers that open their APIs (Yahoo! actually). To do so, there are always limitations to keep in mind in terms of call frequency per IP or per authenticated user. Our application will also have its community inside of which financial data will be shared. For financial data providers, when talking about a given stock, the historical view of a stock and an instant quote of a stock are two different things. We had to deal with these two concepts to build our own data set.
In our application, Users
will be able to buy and sell Products
(stock, fund, option, and so on) by executing Transactions
:
User
entity can have many Transactions
Entities.In the User class, the second part of the @OneToMany
relationship annotation (the Many
element) drives the Type of attribute we are creating. Specifying Many
as the second part declares that the origin entity (User
) can have several target Entities (Transactions
). These targets will have to be wrapped in a collection type. If the origin entity cannot have several targets, then the second part of the relationship has to be One
.
Transaction
can have only one User
entity.Still in the User class, the first part of the @OneToMany
relationship (the @One
element) is the second part of the relationship annotation defined in the target entity (if defined). It is necessary to know whether the target entity can have several origins or not, in order to complete the annotation in the origin.
@OneToMany
in User
and @ManyToOne
in Transactions
.@ManyToMany
relationship, we are talking about a unidirectional relationships. From a database's point of view, this means that one of the two tables having a join column that targets the other table. In the JPA, the table that has this join column is the relationship's owner.Transaction
:@ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "user_name") private User user;
The user_name
column is expected (or automatically added) in the transaction table. We will talk about the fetch type later in the There’s more… section.
User
entity is defined as follows:@OneToMany(mappedBy="user", cascade ={CascadeType.ALL}, fetch = FetchType.LAZY) @OrderBy("id desc") private Set<Transaction> transactions = new LinkedHashSet<>();
An Index
entity has one Market
entity. We have decided that a market is the geographical area (Europe, the US, Asia, and so on). A market has several concrete indices.
This looks like a @OneToMany
/@ManyToOne
relation again. The relationship's owner is the Index
entity because we expect to have a Market
column in the Index
table (and not an Index
column in the Market
table).
It is the same story between the concrete Product
(such as StockProduct
) and Market
entities, except that, since it doesn't look mandatory in the application to retrieve stocks directly from Market
, the relationship has not been declared on the Market
entity side. We have kept only the owner side.
About the concrete Quotes
entity (such as StockQuote
) and the concrete Products
entity (such as StockProduct
), one quote will have one product. If we were interested in retrieving Quote
from a Product
entity, one product would have many quotes. The owner of the relationship is the concrete Quote
entity.
It is the same logic as the previous point for IndexQuote
and Index
.
Between Index
and StockProduct
, in reality, indices (S&P 500, NASDAQ, and so on) have constituents, and the sum of the constituents' values makes the index value. Thus, one Index
entity has several potential StockProduct
entities. Also one StockProduct
can belong to several Indices
. This looks like a bidirectional relationship. We present here the Index
side:
@ManyToMany(fetch = FetchType.LAZY) @JoinTable(name = "stock_indices", joinColumns={@JoinColumn(name = "index_code")}, inverseJoinColumns={@JoinColumn(name ="stock_code")}) private Set<StockProduct> stocks = new LinkedHashSet<>();
This relationship is specified an extra join table (expected or generated by the JPA). It is basically a table with two join columns pointing to the @Ids
fields of the respective entities.
We are going to visit two metadata attributes that we didn’t explain yet: the FetchType
attribute and the Cascade
attribute.
We have seen that the relationship annotations @OneToOne
, @OneToMany
, and @ManyToMany
can be specified in a fetch attribute, which can either be FetchType.EAGER
or FetchType.LAZY
.
When a FetchType.EAGER
attribute is chosen, relationships are automatically loaded by the entityManager
when the entity gets managed. The overall amount of SQL queries executed by JPA is significantly increased, especially because some related entities that may not be required each time are loaded anyway. If we have two, three, or more levels of entities bound to a root entity, we should probably consider switching some fields locally to FetchType.LAZY
.
A FetchType.LAZY
attribute specifies the JPA implementation not to populate a field value on the entity-loading SQL query. The JPA implementation generates extra-asynchronous SQL queries to populate the LAZY
fields when the program specifically asks for it (for example, when getStock()
is called in the case of a HistoricalStock
entity). When using Hibernate as an implementation, FetchType.LAZY
is taken as the default fetch type for relationships.
It is important to think about lightening the relationship loading, especially on collections.
Another attribute to be mentioned in relationship annotations is the Cascade attribute. This attribute can take the values CascadeType.DETACH
, CascadeType.MERGE
, CascadeType.PERSIST
, CascadeType.REFRESH
, CascadeType.REMOVE
, and CascadeType.ALL
.
This attribute specifies how the JPA implementation should process the related Entities when asked to perform an operation (such as persist, update, delete, find, and so on.) on the main Entity. It is an optional attribute which is usually defaulted to no-cascaded operations.
There is a third strategy to define Entity inheritance: