In addition to adding a layer of abstraction above the database system, Hibernate introduces state management to entity objects. This shifts the focus of development from executing SQL statements to managing the state of entities.
In Hibernate, an entity can have one of the following four states:
The transitions between the states are triggered by calling various methods on the session, as shown in the following diagram:
There are even more methods of loading an entity from the database using the query interface or a set of interfaces that retrieve an entity by its identifier. These are not shown in the diagram, but will be discussed together with the shown methods in the following sections.
New instances of an entity class start in the transient state. A transient entity can be persisted by associating it with a session and calling the save
or persist
method:
Account account = new Account("John", "Doe", "[email protected]", "pass1234"); Session session = SessionFactoryHelper.getSessionFactory().openSession(); Transaction transaction = session.beginTransaction(); Serializable accountID = session.save(account); transaction.commit(); session.close();
In this example, after creating a new account and obtaining an opened connection, a transaction is started. This is important for Hibernate to be able to roll back the changes that fail while cascading a save
or update
command over the object graph. It is possible to configure the connections to be in the autocommit mode by adding an appropriate property to the Hibernate configuration:
<property name="connection.autocommit">true</property>
This makes it unnecessary to wrap the save or update commands in a transaction. However, this should only be done in an application that does not need to cascade the changes over the object graph.
Once inside a transaction, the account can be persisted by passing it to the session's save
method, which returns the generated identifier. Finally, the transaction is committed, which implicitly calls the flush
method on the session. Then the session can be closed. Now the entity has a representation in the database.
Another way to persist an object is by using the persist
method instead of save
. Both methods differ slightly in their behavior outside of transactions and of assigning identifiers:
save
:persist
:flush
methodflush
method has to be called explicitly to write an object to the databaseTo load an entity from the database, either the get
or load
method can be used. Both expect the entity class and an identifier as parameters, and return an instance of an object, which must be cast to the target class type:
int accountID = 1; Account account1 = (Account)session.get(Account.class, accountID); Account account2 = (Account)session.load(Account.class, accountID);
It is also possible to load a database entry into an existing object:
Account account = (Account)session.load(Account.class, 1); session.load(account, 2);
While the get
method returns null
, if no object with the given identifier exists in the database, load returns an instance of the target class in any case, and fails with an ObjectNotFoundException
when accessing the entities fields. Thus, it should only be used when it is certain that the entry to be loaded exists in the database.
Finally, an entity can be reloaded from the database. This can be useful if a table uses triggers to calculate or modify values when inserting or updating an entry. This is done by calling the refresh
method of the session:
session.refresh(account);
It is often needed to get a list of entries from the database that match certain criteria. This can be done by letting a session create a Query
object and calling its list()
method:
Query query = session.createQuery("from Account"); List accounts = query.list();
The returned list will then contain all the entries from the table that is mapped to the Account
class.
To filter the result, a where constraint containing a single parameter or parameter-lists can be added to the query:
Query query = session.createQuery("from Account where lastName = :lastName and firstName in (:firstNames)"); query.setString("lastName", "Doe"); List firstNames = new ArrayList(); firstNames.add("John"); firstNames.add("Jane"); query.setParameterList(":firstNames", firstNames); List accounts = query.list();
It is also possible to paginate the returned results by setting the first entry and/or the maximum number of rows returned by a query before calling the list method:
query.setFirstResult(20); query.setMaxResults(10); query.list();
Besides specifying queries when calling the createQuery
method, named queries can be defined by adding an appropriate annotation to the Entity
class:
@Entity @Table(name = "account") @NamedQuery(name="Account.byLastName", query="from Account where lastName = :lastName") public class Account{ //... }
Such named queries are global within the scope of the SessionFactory
, and an appropriate Query
object can be created using the session's createNamedQuery
method:
Query query = session.createNamedQuery("Account.byLastName");
This query can be used in the same way as the one created by explicitly specifying a query.
In many applications, it is useful to construct a where constraint at runtime. Rather than building the query string for such constraints, the criteria
API in Hibernate can be used:
Criteria criteria = session.createCriteria(Account.class); criteria.add(Restrictions.eq("lastName", "Doe")); criteria.setMaxResults(10); criteria.addOrder(Order.asc("lastName")); List list = criteria.list();
After creating a criteria object for the Account
class, an equals restriction is added to the lastName
field. Then the result is limited to ten entries, and ascending ordering is applied. Finally, the query is executed by calling the list()
method.
The Restrictions
class provides methods for all the constraints that can be used in a WHERE
statement. It also defines the Boolean operations that can be used to combine the constraints:
Restrictions.and( Restrictions.gt("number_of_owners", 1), Restrictions.le("number_of_owners", 3), Restrictions.not( Restrictions.in("car_id", new Integer[]{1, 5, 10}) ) );
The preceding example creates a restriction for a query on the car
table that translates to a WHERE
constraint in the form of:
WHERE number_of_owners > 1 AND number_of_owners <= 3 AND car_id NOT in (1, 5, 10);
To select cars with one to three owners, excluding certain car_id
. Again, there is an equivalent for all Boolean operations that can be used in a WHERE
constraint.
Entities in a persisted state can be modified by updating the object's fields. Any changes will then be written to the database when the session is flushed by committing the transaction:
Account account = (Account)session.get(Account.class, 18); Transaction transaction = session.beginTransaction(); account.changePassword("newPass123"); transaction.commit();
However, this requires loading and updating of an entity to happen within the same session. This might be inefficient if there is a longer period of time between loading and updating, for example, when displaying an entry in a user interface for entering the changes. In such cases, a more efficient approach is to update a detached entity:
public void updateAccount(int accountID){ Session session =SessionFactoryHelper .getSessionFactory().openSession(); Account account = (Account)session.get(Account.class, accountID); session.close(); displayUI(account); session = SessionFactoryHelper.getSessionFactory().openSession(); Transaction transaction = session.beginTransaction(); session.update(account); transaction.commit(); session.close(); } private void displayUI(Account account){ //display user-interface to modify the account }
This loads the account in the first session, and then detaches it by closing the session. After that, the user interface can be displayed without blocking any database connections. Finally, the entity is reattached, and the updates are written to the database by calling the update
method on a second session.
When reattaching an entity, it is important that a persistent entity with the same identifier loaded within the sessions scope does not exist already. Otherwise, an exception will be thrown.
To reattach an entity in any case, the merge
method can be used; doing so will overwrite an already existing entity.
To delete an entity, the session interface provides the delete
method:
Transaction transaction = session.beginTransaction(); session.update(account); transaction.commit();
After calling delete
, the entity is in a detached state. It will only be deleted from the database when the session is flushed during the committing of the transaction. The application can still hold a reference to the object, which can be used like any other object in transient state.
In relational databases, a table can reference the entries from another table. This is usually done by defining a foreign key constraint that references a unique identifier of the referenced table. There are four kinds of mappings, which will be introduced in the following sections.
In a one-to-many mapping, one entity can be associated with multiple entities, while in a many-to-one mapping, several entries of one table are mapped to an entry in another one. In the car_portal_app
schema, the tables account
and account_history
are associated this way. To implement such a relation with Hibernate, the Entity
class for account_history
has to be created and added to the Hibernate configuration first:
@Entity @Table(name = "account_history") public class AccountHistory{ @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "account_history_id") private int accountHistoryID; @Column(name = "search_key") private String searchKey; @Column(name = "search_date") private Date searchDate; @ManyToOne @JoinColumn(name="account_id") private Account account; AccountHistory(){} public AccountHistory(String searchKey, Date searchDate, Account account){ this.searchKey = searchKey; this.searchDate = searchDate; this.account = account; } }
The implementation is very similar to the Account
class, the only difference being the account field which references the associated account. This field is annotated with @ManyToOne
indicating the kind of association. Also, the column referenced by the foreign key constraint is defined using the optional @JoinColumn
annotation. If this annotation is omitted, Hibernate will expect a column name in the format of <referenced table>_<referenced column>
, which would be account_account_id
in this case.
Referencing the Account
entity in AccountHistory
defines a unidirectional relation. An entity of AccountHistory
knows about the account it is associated with, but an entity of Account
does not know its history.
To achieve this, a bidirectional relation has to be created. This is done by complementing the Account
class with a field referencing the associated account_history
entries, and a method to access the mapping:
@OneToMany(cascade = CascadeType.ALL, mappedBy = "account") private List<AccountHistory> accountHistory = new LinkedList<>(); public Set<AccountHistory> getHistory(){ return accountHistory; }
The @OneToMany
annotation is configured with the mappedBy
attribute, which refers to the account field in AccountHistory
. This tells Hibernate that the foreign key constraint is contained in the account_history
table, and thus, makes AccountHistory
the owner of the relation.
Also, the @OneToMany
annotation is configured with the optional cascade
attribute, which causes Hibernate to cascade transitions between the states of an Account
entity to the associated AccountHistory
entities. Without defining a cascade type, this would have to be done explicitly. There are CascadeTypes
for each transition between states (MERGE
, PERSIST
, REFRESH
, DETACH
, REMOVE
) that can be used if only certain transitions should be cascaded. CascadeType.ALL
combines all of them.
Hibernate will now load the associated history items when an account is read from the database. To add entries to the history from within the application, a new instance of AccountHistory
has to be created and added to the account that it is associated to:
Session session = SessionFactoryHelper.getSessionFactory().openSession(); Transaction transaction = session.beginTransaction(); Account account = (Account)session.load(Account.class, 1); AccountHistory history = new AccountHistory( "search term", new Date(), account); account.getHistory().add(history); session.update(account); transaction.commit(); session.close();
In the preceding example, the AccountHistory
item is still transient after creating it and adding it to the account. It is persisted once the account is updated.
In a one-to-one mapping, an entity is associated to exactly one entity of another table and vice versa. The implementation does not differ much from that of the one-to-many and many-to-one mapping shown in the previous section. The only difference is that the respective fields are objects, and have to be annotated with @OneToOne
on both sides.
Since the entries in a one-to-one mapping are often very strongly related to each other, they can alternatively be stored in the same table. In the car_portal
database, the address data in the seller_account
table is such a case. On the Javaside, it is nonetheless possible to group the columns that belong together in a dedicated class. For the address data, such a class could be implemented like this:
@Embeddable public class Address{ @Column(name = "street_name") private String streetName; @Column(name = "street_number") private String streetNumber; @Column(name = "zip_code") private String zipCode; private String city; }
The @Embeddable
annotation tells Hibernate that the Address
class will be used in a component mapping. It can then be referenced from an entity class using the @Embedded
annotation:
@Embedded private Address address;
The embeddable entity cannot use the @Id
annotation to define an identifier, nor can it be associated with a specific table using the @Table
annotation. Instead, both are inherited from the enclosing entity class.
Moreover, as with value types, shared references are not being used. Even if two seller_accounts
have the same address data, their entities will contain independent instances of Address
.
In a many-to-many mapping, both sides of the association can reference multiple entries of the other side. In the car_portal_app
schema, the table favorite_ads
defines such a relation by associating the account
tables and advertisement using their primary keys. Implementing the Advertisement
entity, this table is configured using the @JoinTable
annotation:
@Entity @Table(name = "advertisement") public class Advertisement{ @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "advertisement_id") private int advertisementID; @Column(name = "advertisement_date") private Date advertisementDate; @Column(name = "car_id") int carID; @Column(name = "seller_acount_id") int sellerAccountID; @ManyToMany(cascade = CascadeType.ALL) @JoinTable(name="favorite_ads", joinColumns={@JoinColumn(name="advertisement_id")}, inverseJoinColumns={@JoinColumn(name="account_id")}) private Set<Account> accounts = new HashSet<>(); Advertisement(){} public Advertisement(Date advertisementDate, int carID, int sellerAccountID){ this.advertisementDate = advertisementDate; this.carID = carID; this.sellerAccountID = sellerAccountID; } }
The name
attribute of the @JoinTable
annotation sets the table containing the mapping, while the attributes joinColumns
and inverseJoinColumns
configure the columns referring the primary keys of the entity owning the association and that of the associated entity, respectively.
In the Account
class, the association is added in a similar way, but with the names of joinColumns
swapped:
@ManyToMany(cascade = CascadeType.ALL) @JoinTable(name = "favorite_ads", joinColumns={@JoinColumn(name = "account_id")}, inverseJoinColumns={@JoinColumn(name = "advertisement_id")}) private Set<Advertisement> advertisement = new HashSet<>();
Now, modifying the set of associated entities in either of the classes and committing those changes will automatically update the mapping-table accordingly.
When loading an entity from the database, there are several strategies regarding when and how to load the associated entities. Choosing the right strategy is important, because choosing the wrong strategy might result in inefficient queries, or the loading of a huge portion of the database if there are associations between a lot of tables. While the default strategies in Hibernate offer a good performance in most cases, it is possible to override them for each mapping.
Hibernate distinguishes between two types of strategies: the fetch
type, which configures when the associated entities are loaded, and the fetch
mode, which defines how the database is queried.
The fetch
type defines when the associated entities are to be loaded, and is configured as an attribute of the annotations that define the mapping type:
@OneToMany(fetch = FetchType.EAGER)
FetchType
defines two strategies:
FetchType.EAGER
: This loads the associated entities immediatelyFetchType.LAZY
: This loads the associated entities when they are accessed for the first timeBy default, one-to-one and many-to-one relations are configured with FetchType.EAGER
, while one-to-many and many-to-many relations are configured with FetchType.LAZY
. For most cases, this is a good choice.
Configuring the one-to-many or many-to-many relations for eager fetching should be done with care, because loading all the associated entities might have a significant impact on performance.
The fetch mode defines how Hibernate creates SQL queries to load the associated entities from the database. It can be configured by adding the @Fetch
annotation to a mapping field:
@OneToMany @Fetch(FetchMode.JOIN)
FetchMode
defines the following modes:
FetchMode.SELECT
This is the default mode. It creates two separate queries similar to the following:
SELECT * FROM account WHERE account_id = ?; SELECT * FROM account_history WHERE account_id = ?;
The first query loads the entity, and the second one loads the mapped entities (lazy or eager).
FetchMode.JOIN
Loads the entity and also the mapped entities using a JOIN
query on both the tables:
SELECT * FROM account LEFT OUTER JOIN account_history ON account.account_id = account_history.account_id WHERE account.account_id = ?;
FetchMode.SUBSELECT
Loads the associated entities for all the entities of a type using a subselect:
SELECT * FROM account; SELECT * FROM account_history WHERE account_history.account_id IN (SELECT account_id FROM account);
Depending on the configuration, this can be done using lazy or eager, which can be added to a mapping.
In addition to the fetch modes, Hibernate provides the @BatchSize
annotation:
@OneToMany @BatchSize(size = 5)
This defines the number of mappings that should be preloaded when iterating over the entities, but not the number of entities in a single mapping. When iterating over accounts and accessing their history, this would result in the following queries:
SELECT * FROM account; SELECT * FROM account_history WHERE account_id IN (?, ?, ?, ?, ?);
Then, after accessing the histories of five accounts, the second query will be executed again to preload the histories for the next accounts, as needed.
In applications that have a lot of users or that have to access large databases, it is often necessary to optimize the database usage in order to limit the used resources or to increase the application's response time.
This section shows how to configure Hibernate to use caches and connection pools, and how to deal with partitioned tables.
A cache is a layer between the application and the database that stores queried data to minimize database access when accessing such data again. Hibernate uses a multi-level caching schema with a session acting as a first-level cache, which caches the changes to an object and delays the write operations as long as possible.
Optionally, Hibernate can be configured to use a second-level cache which is consulted after a lookup if the first-level cache yields no result and before the underlying database is queried. Any third-party cache that implements Hibernate's CacheProvider
interface can be used. Hibernate is bundled with EhCache
and Infinispan
; other frequently used opensource implementations of CacheProvider
are OSCache and Terracotta.
The CacheProvider
to be used is configured by adding an appropriate property to the session-factory node of the Hibernate configuration:
<hibernate-configuration> <session-factory> <property name="hibernate.cache.provider_class"> org.hibernate.cache.EhCacheProvider </property> </session-factory> </hibernate-configuration>
Depending on the implementation used, it will also be necessary to create a configuration file for it. In case of EhCache
, a file ehcache.xml
has to be created in the application's classpath:
<diskStore path="java.io.tmpdir"/> <defaultCache maxElementsInMemory="1000" eternal="false" timeToIdleSeconds="120" timeToLiveSeconds="240" overflowToDisk="true" />
The preceding configuration tells EhCache to store up to 1,000 entries for each entity-class in the memory. If this number is exceeded, cached data will be written to the disk. It also defines that entries should be removed from the cache if they have not been accessed for 120 seconds, or after 240 seconds regardless of whether they have been accessed or not.
After specifying the CacheProvider
, the next step is to configure the concurrency strategy. The concurrency strategy defines the way data is stored in the cache, and how it is to be retrieved and configured by adding the @Cache
annotation to a class or a collection:
@Entity @Table(name="account") @Cache(usage=CacheConcurrencyStrategy.READ_WRITE) public class Account{ //... }
The possible strategies are as follows:
The decision if caching should be enabled and the strategy to use should be made separately for each entity class. As caching may sometimes even have a negative impact on performance, benchmarks should be taken and caching should only be used if the performance increases remarkably.
Opening a connection to a database is an expensive operation in terms of runtime, and can have quite an impact on performance. Therefore, Hibernate allows the use of a connection pool that cares for creating connections as needed, and reuses the open connections instead of closing them after each operation.
Hibernate comes with a built-in connection pool that can be enabled by simply adding the pool_size
property to the session-factory configuration:
<property name="connection.pool_size"> 100 </property>
However, Hibernate's pooling mechanism is not very efficient and is not meant to be used in a productive environment.
Instead, a third-party implementation like c3p0
, which comes bundled with Hibernate, should be used. A third-party connection pool is configured by replacing the pool_size
property with the implementation-specific configuration.
For c3p0
, the following properties can be added to the session-factory configuration:
<property name="hibernate.c3p0.min_size">5</property> <property name="hibernate.c3p0.max_size">20</property> <property name="hibernate.c3p0.timeout">300</property> <property name="hibernate.c3p0.max_statements">50</property> <property name="hibernate.c3p0.idle_test_period">3000</property>
This configures the following parameters of c3p0
:
min_size
: The minimum number of connections the pool should contain (default: 1
).max_size
: The maximum number of connections the pool should contain (default: 100
).timeout
: The number of seconds after which an unused connection expires and is removed from the pool. The default value of 0 never expires connections.max_statements
: The number of prepared connections that are pooled (default: 0
).idle_test_period
: Sets the number of seconds after an idle connection is validated.Applications running inside an application-server should use one of its configured data-sources instead of a third-party connection-pool. To use a data-source, its JNDI name has to be added to the session-factory configuration like this:
<property name="hibernate.connection.datasource"> java:/car_portal_app/jdbc/test </property>
It replaces the connection.url
property.
To improve the performance from the database side, very large tables are often partitioned. In PostgreSQL, this is done by inheriting a child table from a parent table for each partition. The parent table typically uses triggers or rules to redirect access to the desired partition. The master table itself is usually empty.
As there are no rows inserted into the parent table, PostgreSQL returns a row count of 0. However, Hibernate interprets this as a failed update operation, and throws an exception.
To prevent this problem, the method of checking the returned row count can be changed by adding the @SQLInsert
annotation to the entity class, which defines the query used for inserting a new row and the type of check that will be performed:
@SQLInsert( sql="INSERT into account (first_name, last_name, email, password) VALUES (?,?,?,?)", check=ResultCheckStyle.NONE) public class Account{ //... }
The possible values for the check attribute are:
ResultCheckStyle.COUNT
: Checks the row count returned by an insert operationResultCheckStyle.NONE
: Does not perform any checkResultCheckStyle.PARAM
: Checks the row count that is returned as an output parameter when using a function to insert new rowsFor updates, the
@SQLUpdate
annotation can be used in the same way.
18.117.105.74