Working with entities

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.

States of an entity

In Hibernate, an entity can have one of the following four states:

  • Transient: This is the initial state of an entity after instantiation. It does not have a representation in the database yet, and is not associated with a session.
  • Persistent: An entity that is represented in the database and has an identifier assigned. A persistent entity is tied to a session.
  • Detached: When closing the underlying session, a persistent entity will be detached. It will still exist as an object, but updates to it will not be reflected in the database. It can be reattached to a session later to persist it again.
  • Removed: An object that is scheduled for deletion.

The transitions between the states are triggered by calling various methods on the session, as shown in the following diagram:

States of an entity

Transitions between Hibernate states

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.

Making a new entity persistent

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:
    • Immediately assigns an identifier
    • Calls flush if used outside a transaction
  • persist:
    • Does not guarantee the assigning of an identifier immediately Depending on the configuration, this might not happen until you call the flush method
    • The flush method has to be called explicitly to write an object to the database

Loading an entity from the database

To 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);

Loading a list of entries

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.

Tip

The names in the query passed to the createQuery method refer to the entity class and its fields, not the mapped table and its columns!

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();

Named queries

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.

Creating dynamic queries

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.

Modifying entities

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.

Deleting entities

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.

Using association mapping

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.

One-to-many and many-to-one mappings

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.

One-to-one mapping and component mapping

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.

Many-to-many mapping

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.

Fetching strategies

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.

Configuring the fetch type

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 immediately
  • FetchType.LAZY: This loads the associated entities when they are accessed for the first time

By 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.

Configuring the fetch mode

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 = ?;

Note

Using FetchMode.JOIN disables lazy fetching!

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.

Tuning the performance of Hibernate

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.

Using caching

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:

  • Read only: Only allows read access. This is the optimal strategy when an application does not need to modify the instances of an entity class.
  • Read/write: This allows the cached entries to be modified, and locks them until they have been completely written to the database. Access to a locked entry will be handled as a cache-miss and propagated to the database.
  • Non strict read / write: An entry being changed will be marked as invalid in the cache and reread from the database upon the next access. However, there is no strict isolation between concurrent access and the changes that have not been written to the database yet, which might become visible to the other transactions. This strategy should not be used if strict transaction isolation is required.
  • Transactional: Uses a two-phase commit, first to the cache and then to the database. This strategy is not supported by every cache implementation.

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.

Using connection pools

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.

Dealing with partitioned tables

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 operation
  • ResultCheckStyle.NONE: Does not perform any check
  • ResultCheckStyle.PARAM: Checks the row count that is returned as an output parameter when using a function to insert new rows

For updates, the @SQLUpdate annotation can be used in the same way.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.117.105.74