Skip to content

Best Hibernate interview preparation material with examples and sql outputs

Notifications You must be signed in to change notification settings

altafjava/hibernate-interview-preparation

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

21 Commits
 
 
 
 

Repository files navigation

  1. JDBC vs Hibernate.

  2. Session vs SessionFactory.

  3. How to create Session & SessionFactory?

  4. SessionFactory vs EntityManagerFactory.

  5. What is Dialect?

  6. What is an Entity Lifecycle?

    Transient, Persistent/Managed, Detached, Removed

    Transient->Persistent save(), persist(), saveOrUpdate()
    Persistent->Transient delete()
    Detached->Persistent merge(), update(), saveOrUpdate(), replicate(), lock()
    Persistent->Removed remove()
    DB->Persistent find(), get(), load()
    Removed->Persistent save(), persist()
  7. Can we create an Entity class without primary key columns?

  8. What are the rules for creating an Entity class?

    1. No-Arg Constructor
    2. Provide an identifier properly
    3. Declare getters/setters
    4. Entity class should not be final because Hibernate uses proxies concept of lazy fetching
  9. Is SessionFactory thread safe? Yes

  10. Is Session thread safe? No

  11. What is the CREATE TABLE query generated by Hibernate?

create table Employee (employeeId integer not null auto_increment, email varchar(255), firstName varchar(255), lastName varchar(255), primary key (employeeId)) engine=MyISAM
  1. What is the INSERT query generated by Hibernate?
insert into Employee (email, firstName, lastName) values (?, ?, ?)
  1. What are Entity Lifecycle events & callbacks?

    Whenever we call methods in the Session interface to persist, update or delete the entities, the session generates an appropriate event based on the executed method and passes it to the configured event listener(s) for that type. The event types are declared as enum values on org.hibernate.event.spi.EventType. For example, when we persist an entity using the session.persist() method then an event EventType.PERSIST is generated. If there are any PersistEventListener implementation registered for that Entity then the event is passed to that listener for processing.

  2. How to create EventListener? Create a class and implements any the of the EventListerner interface and override its methods. For example PersistEventListener is the interface for PERSIST event.

    public void onPersist(PersistEvent persistEvent) throws HibernateException
    public void onPersist(PersistEvent event, PersistContext createdAlready) throws HibernateException
  3. How to register EventListener?

    To register the event listeners, we need to create our own implementation of org.hibernate.integrator.spi.Integrator interface. The main use of Integrator is to register the event listeners. Create a class and implements org.hibernate.integrator.spi.Integrator and override its methods.

      public void integrate(Metadata metadata, SessionFactoryImplementor sessionFactory, SessionFactoryServiceRegistry serviceRegistry)
    
      public void integrate(Metadata metadata, BootstrapContext bootstrapContext, SessionFactoryImplementor sessionFactory)
    
      public void disintegrate(SessionFactoryImplementor sessionFactory, SessionFactoryServiceRegistry serviceRegistry)

    The Integrator is then registered with the persistent context with the help of BootstrapServiceRegistryBuilder. Note that BootstrapServiceRegistry is intended to hold mainly 3 services that Hibernate needs at both bootstrap and run time.

    1. ClassLoaderService
    2. IntegratorService
    3. StrategySelector.

    After creating the BootstrapServiceRegistery, Now supply the custom BootstrapServiceRegistry instance to the StandardServiceRegistry and build the SessionFactory.

    BootstrapServiceRegistryBuilder bootstrapServiceRegistryBuilder = new BootstrapServiceRegistryBuilder();
    
    bootstrapServiceRegistryBuilder.applyIntegrator(new AppIntegrator());
    
    BootstrapServiceRegistry bootstrapServiceRegistry = bootstrapServiceRegistryBuilder.build();
    
    StandardServiceRegistry standardServiceRegistry = new StandardServiceRegistryBuilder(bootstrapServiceRegistry).configure().build();
    
    Metadata metadata = new MetadataSources(standardServiceRegistry).getMetadataBuilder().build();
    
    SessionFactory sessionFactory = metadata.getSessionFactoryBuilder().build();
  4. How to check Entity equality between Sessions?

    Requesting a persistent/managed object again from the same Hibernate session returns the “same instance” of a class. Requesting a persistent object from the different Hibernate sessions returns the "different instances" of a class. Hence as a best practice, always implement equals() and hashCode() methods in the hibernate entities; and always compare them using equals() method only.

  5. save()

    This method is used to save Entity into DB and returns the primary key. This makes a TRANSIENT entity to PERSISTENT. We can use this method outside a transaction. Means without transaction also we can save the entity. If we have multiple entities with mapping among them(OTO, OTM, MTO, MTM) and if we are saving outside the transaction then only primary(parent) entity will be saved. Lets say We have Cart entity mapped with Item entity as One to Many. Cart can have multiple items. If we are saving cart without transaction then only Cart entity will be saved. Item entity will not be saved. Even we start the transaction and don’t flush or commit then still Item will not be saved. We must either flush or commit then mapped entity will be saved. Hence we should avoid save outside transaction boundary. This will led the data inconsistency problem.

    We can store a DETACHED entity using this method. It will create a new primary key for that Detached entity but the associated entity will not create a new record(new primary key) ; rather for the associated entity it will execute an update query which does nothing.

    Session session = sessionFactory.openSession();
    Transaction transaction = session.beginTransaction();
    session.save(employee);
    transaction.commit();
    transaction = session.beginTransaction();
    session.evict(employee);
    session.save(employee);
    transaction.commit();
    Hibernate: insert into Employee (firstName, lastName, salary) values (?, ?, ?)
    Hibernate: insert into Address (city, zipcode, id) values (?, ?, ?)
    Hibernate: insert into Employee (firstName, lastName, salary) values (?, ?, ?)
    Hibernate: update Address set city=?, zipcode=? where id=?
  6. persist()

    This method is also used to save the entity into DB(Transient to Persistent) but it does not return anything and it works only inside the transaction. If we have mapped entities and we have begun the transaction but did not flush or commit then only the parent entity will be saved not the child entity. To save all the mapped entities we must commit or flush the session. We cannot store a DETACHED entity using this method. We shall get exception

  7. saveOrUpdate()

    This method is used for either insert or update queries based on the provided data. If the data is present in the database(Means it is a Persistent entity) then an update query is executed. If the data is not present in the database(means it is a Transient entity) then insert query will be executed. If we have a Persistent entity and we did not change anything in those entities then nothing will happen(neither insert or update).

    We can use this method without transaction too, but again we shall face the issues with mapped objects. Only the primary(parent) entity will be saved. If we want to save all the mapped entities then we must start the transaction and either flush or commit. It does not return anything. We cannot store a DETACHED entity using this method. If we try to do so then it will execute an update query which does nothing.

  8. update()

    This method should be used where we know that we are only updating the entity information. This will only work with Persistent entities. If we are trying to update a Transient entity and a transaction is started and we are doing either flush or commit then we shall get an exception. If a transaction has started and we are doing flush/commit then nothing will happen.

    If the entity is in Persistent state and we are modifying anything then nothing will happen. This method only works with transactions. We must start the transaction and either flush or commit. Otherwise nothing will happen. If we are updating the Persistent entity inside the transaction and the updated data is the same with the Persistent entity then it will not execute the update query. It will remain the same.

  9. merge()

    The main purpose of the merge method is to make a DETACHED entity to PERSISTENT. Without a transaction it does not work. We must start the transaction. This method is almost the same as saveOrUpdate except it returns the PERSISTENT entity and does not throw NonUniqueObjectException.

    If we have a DETACHED entity Employee and we want to make this as PERSISTENT entity and that session already has and entity with the same id(primary key) with that DETACHED entity, we shall get NonUniqueObjectException if we shall use update() or saveOrUpdate() methods. But if we use the merge() method then it will work perfectly without any exception.

    Session session = sessionFactory.openSession();
    Transaction transaction = session.beginTransaction();
    Employee employee = session.get(Employee.class, 2);
    session.evict(employee);
    employee.setFirstName("James");
    Employee employee2 = session.get(Employee.class, 2);
    // session.saveOrUpdate(employee); // NonUniqueObjectException
    // session.update(employee); // NonUniqueObjectException
    session.merge(employee);
    Employee mergedEmployee = session.merge(employee);

    If we are saving an TRANSIENT Employee entity which has an associated entity Address and we did not flush/commit then only the Employee entity will be saved. If we do either flush or commit then the associated entity Address will be saved.

    If we want to update the Persistent entity then we must start the transaction and flush/commit then will work. If we are updating the Persistent entity inside the transaction and the updated data is the same with the Persistent entity then it will not execute the update query. It will remain the same. If we are updating the Persistent entity inside the transaction and the updated data is different with the Persistent entity then it will execute the update query.

  10. Get vs Load

    The entity loaded with get() method is eager loaded and contains information from the database. Whereas the entity loaded from load() method is a lazy load proxy object that queries the database when any field information is accessed. Hibernate internally uses ByteBuddy to create proxy classes at runtime.

    In the case of the get() method, we will get the return value as NULL if the identifier is absent. But in the case of load() method, we will get an ObjectNotFoundException.

    load() method is deprecated since Hibernate 6.0. It is recommended to use the getReference() method instead of the load() method.

    Hibernate uses LEFT JOIN to fetch the single entity.
    select e1_0.id,a1_0.id,a1_0.city,a1_0.zipcode,e1_0.firstName,e1_0.lastName,e1_0.salary from Employee e1_0 left join Address a1_0 on e1_0.id=a1_0.id where e1_0.id=?
  11. refresh()

    Sometimes we face a situation when our application database is modified with some external application/agent and thus corresponding hibernate entity in our application actually becomes out of sync with its database representation i.e. having old data. In this case, we can use the session.refresh() method to re-populate the entity with the latest data available in the database.

  12. delete()/remove()

    Both methods are used to delete a single entity. If the passed entity is not present in the database then we shall get an ObjectNotFoundException. Both methods work on DETACHED entities as well if we are using Hibernate Session. But in the case of JPA, the EntityManager delete() method cannot delete the DETACHED entity. This will give IllegalArgumentException.

     EmployeeEntity employee = entityManager.find(Employee.class, 1L);
     entityManager.detach(employee);
     entityManager.remove(employee);

    If there is any associated entity, then first associated entity will be deleted then the parent entity. delete() method is deprecated since Hibernate 6.0. It is recommended to use the remove() method.

    Hibernate: delete from Address where id=?
    Hibernate: delete from Employee where id=?

    The EntityManager.remove() and Session.remove() methods are a better fit when we want to delete a single Entity but inefficient if we want to remove a list of entities. For each remove() call, hibernate loads the entity, performs the lifecycle transition to REMOVED and triggers the SQL DELETE operation. Executing N different SQL DELETE queries for N entities will lead to very inefficient performance. It’s often better to remove such a list of entities with a JPQL query.

    We should flush all the pending changes to the database before executing the query so that we are making changes to the latest data. And we also minimize the risk of stale first-level cache.

    session.flush();
    session.clear();
    //Get list of Ids
    List<Long> ids = getIdsToDelete();
    //Remove all entities
    Query query = em.createQuery("DELETE EmployeeEntity e WHERE id IN (:ids)");
    query.setParameter("ids", ids);
    query.executeUpdate();
  13. @Entity

    This annotation is used to make the POJO class as JPA Entity. Entity class must have a no-argument constructor without private access modifier, otherwise we shall get NoSuchMethodException. Entity class must not be an abstract class because Hibernate creates the object of Entity class by using newInstance() method and as we know Abstract class cannot be instantiated. We can declare Entity class as final but this is not recommended. Hibernate uses the proxy pattern for performance improvement during lazy association. By making an entity final, Hibernate will no longer be able to use a proxy as Java doesn't allow the final class to be extended.

  14. @Id

    Each entity bean has to have a primary key, which we annotate on the class with the @Id annotation. We can use this annotation either in a field or in a setter method.

  15. @GeneratedValue

    This annotation is used when we want Hibernate to assign the entity identifier automatically using either AUTO, IDENTITY, SEQUENCE, TABLE. If we don’t use this annotation, then the identifier must be manually assigned before saving the entity.

    AUTO: Indicates that the persistence provider should pick an appropriate strategy for the particular database.

    IDENTITY: Indicates that the persistence provider must assign the primary key for the entity using a database identity column. Ex: MySQL

    SEQUENCE: Indicates that the persistence provider must assign primary keys for the entity using a database sequence. It uses @SequenceGenerator. Ex: Oracle

    TABLE: Indicates that the persistence provider must assign primary keys for the entity using an underlying database table to ensure uniqueness. In this case, one extra table is created by Hibernate with the name hibernate_sequences and there will be two columns sequence_name and next_val. It internally uses @TableGenerator.

  16. @SequenceGenerator

    A sequence is a database object that can be used as a source of primary key values. It is similar to the use of an identity column type, except that a sequence is independent of any particular table and can therefore be used by multiple tables. Some of the databases don’t support sequences. In this case the database sequence concept will not be utilized and it creates a new table like @TableGenerator.

    @Id
    @SequenceGenerator(name="myseq",sequenceName="HIB_SEQ")
    @GeneratedValue(strategy=SEQUENCE,generator="seq")
    private Integer eid;

    In this annotation name attribute is mandatory. A new table will be created with the name hib_seq. If we don’t provide the sequenceName attribute then myseq table will be created.

  17. @TableGenerator

    This annotation is used in a very similar way to the @SequenceGenerator annotation, but because it manipulates a standard database table to obtain its primary key values, instead of using a vendor-specific sequence object, it is guaranteed to be portable between database platforms. It is the same as @GeneratedValue(strategy = GenerationType.TABLE). It creates a table with two columns sequence_name and next_val.

    @Id
    @GeneratedValue(strategy=GenerationType.TABLE,generator="employee_generator")
    @TableGenerator(name="employee_generator", table="pk_table",
    pkColumnName="name", valueColumnName="value", allocationSize=100)
    private Integer eid;

    Only the name attribute is mandatory. Rest are optional.

  18. @Embeddable/@Embedded

    We can use these annotations to embed one entity inside another entity, so they are mapped to a single table. @Embeddable is used to mark a class to be eligible as an embedded class. @Embedded is used in the field/attribute inside the parent class. We can change the column name of the embeddable class inside the parent class using these annotations. @AttributeOverrides & @AttributeOverride.

    @Entity
    Class Employee{
    @Embedded
    @AttributeOverrides(value = @AttributeOverride(name = "city",
    column = @Column(name = "village")))
    private Address address;
    }
    @Embeddable
    Class Address{
    }

Association/Mappings

  1. @OneToOne

    There are primarily 4 ways to create one-to-one relationships between two entities.

    1. The first technique is widely used and uses a foreign key column in one of the tables.
    2. The second technique uses a rather known solution of having a join table to store the mapping between the first two tables.
    3. The third technique is something new that uses a common primary key in both tables.
    4. Using @MapsId annotation that uses common primary key in both the tables but different from 3rd approach. It copies primary key from child table and put into parent table as foreign key.

    Using a Foreign Key Association/Join Column: In this kind of association, a foreign key column is created in the owner entity. For example, we have made Employee entity as an owner, then an extra column accountId will be created in the employee table. This column will store the foreign key for the Account table.

     @Table
     @Entity
     public class Employee implements Serializable {
       @Id
       @GeneratedValue(strategy = GenerationType.IDENTITY)
       private int eid;
       private String firstName;
       private String lastName;
       private double salary;
       @OneToOne(cascade = CascadeType.PERSIST)
       @JoinColumn(name = "accountId") // accountId will become foreign in employee table
       private Account account;
     }
    @Table
    @Entity
    public class Account {
      @Id
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      private int aid;
      private String accountNo;
      private String branch;
    }
    Employee employee = new Employee();
    employee.setFirstName("David");
    employee.setLastName("Warner");
    employee.setSalary(56789);
    Account account=new Account();
    account.setAccountNo("ACC123");
    account.setBranch("Australia");
    employee.setAccount(account);
    
    session.persist(employee);

    In the Employee class if don't cascade the Account class then we shall get an exception saying:

    Exception in thread "main" java.lang.IllegalStateException: org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: com.altafjava.entity.Account

    If we want no cascading then 1st we need to persist the Account entity then persist the Employee entity.

    Account account=new Account();
    account.setAccountNo("ACC123");
    account.setBranch("Australia");
    session.persist(account);
    
    Employee employee = new Employee();
    employee.setFirstName("David");
    employee.setLastName("Warner");
    employee.setSalary(56789);
    employee.setAccount(account);
    session.persist(employee);

    Generated Query:

    Hibernate: create table Account (aid integer not null auto_increment, accountNo varchar(255), branch varchar(255), primary key (aid)) engine=MyISAM
    
    Hibernate: create table Employee (eid integer not null auto_increment, firstName varchar(255), lastName varchar(255), salary float(53) not null, accountId integer, primary key (eid)) engine=MyISAM
    
    Hibernate: alter table Employee add constraint FKcs7jiub5myswnmgqtnp1uj1fi foreign key (accountId) references Account (aid)
    
    Hibernate: insert into Account (accountNo, branch) values (?, ?)
    
    Hibernate: insert into Employee (accountId, firstName, lastName, salary) values (?, ?, ?, ?)
    
    Hibernate: select e1_0.eid,a1_0.aid,a1_0.accountNo,a1_0.branch,e1_0.firstName,e1_0.lastName,e1_0.salary from Employee e1_0 left join Account a1_0 on a1_0.aid=e1_0.accountId where e1_0.eid=?

    If we don't write @JoinColumn in the owner entity Employee then defaults apply. A new column will be created in the parent table Employee with name account_aid. Means concatenation of child table name(account), underscore(_) and child table primary key(aid).

    Hibernate: alter table Employee add constraint FKf5cbit5cfn86kiuergvpbidcr foreign key (account_aid) references Account (aid)

    In a bidirectional association, only one of the sides has to be the owner. The owner is responsible for the association column(s) update. The child entity who is not responsible of managing the relationship uses mappedBy attribute in @OneToOne annotation. The mappedBy refers to the property name which is declared in the owner's side.

    public class Account {
      private int aid;
      private String accountNo;
      private String branch;
      @OneToOne(mappedBy = "account")
      private Employee employee;
    }

    If we don't use this mappedBy attribute then both the table will become owner table and stores the foreign key of each others. Employee will store Account's primary and Account will store Employee's primary key.

      Hibernate: create table Account (aid integer not null auto_increment, accountNo varchar(255), branch varchar(255), employee_eid integer, primary key (aid)) engine=MyISAM
    
      Hibernate: create table Employee (eid integer not null auto_increment, firstName varchar(255), lastName varchar(255), salary float(53) not null, accountId integer, primary key (eid)) engine=MyISAM
    
      Hibernate: alter table Account add constraint FKehpisc8myx4mq7peeu83gl5c4 foreign key (employee_eid) references Employee (eid)
    
      Hibernate: alter table Employee add constraint FKrwcsuf6gqdo5nbhro5avjqg6y foreign key (accountId) references Account (aid)
    
      Hibernate: insert into Account (accountNo, branch, employee_eid) values (?, ?, ?)
    
      Hibernate: insert into Employee (accountId, firstName, lastName, salary) values (?, ?, ?, ?)

    Using a link table/Join Table: In this approach, Hibernate will create a new table/link table that will store the primary key values from both the entities. In this technique @JoinTable is used. This annotation is used to define the new table and foreign keys from both of the tables.

      @Table
      @Entity
      public class Employee  {
         @Id
         @GeneratedValue(strategy = GenerationType.IDENTITY)
         private int eid;
         private String firstName;
         private String lastName;
         private double salary;
         @OneToOne(cascade = CascadeType.PERSIST)
         @JoinTable(name = "employee_account", joinColumns = @JoinColumn(name = "employeeId"), inverseJoinColumns = @JoinColumn(name = "accountId"))
         private Account account;
      }
      @Table
      @Entity
      public class Account {
         @Id
         @GeneratedValue(strategy = GenerationType.IDENTITY)
         private int aid;
         private String accountNo;
         private String branch;
      }
      Hibernate: create table Account (aid integer not null auto_increment, accountNo varchar(255), branch varchar(255), primary key (aid)) engine=MyISAM
    
      Hibernate: create table Employee (eid integer not null auto_increment, firstName varchar(255), lastName varchar(255), salary float(53) not null, primary key (eid)) engine=MyISAM
    
      Hibernate: create table employee_account (accountId integer, employeeId integer not null, primary key (employeeId)) engine=MyISAM
    
      Hibernate: alter table employee_account add constraint FKcf0q6b690ussmm11u4s08nixj foreign key (accountId) references Account (aid)
    
      Hibernate: alter table employee_account add constraint FKnvb8g9m5qlhk1752cq9muifk7 foreign key (employeeId) references Employee (eid)
    
      Hibernate: insert into Account (accountNo, branch) values (?, ?)
    
      Hibernate: insert into Employee (firstName, lastName, salary) values (?, ?, ?)
    
      Hibernate: insert into employee_account (accountId, employeeId) values (?, ?)

    @JoinTable annotation is used in Employee entity class. It declares that a new table employee_account will be created with two columns employeeId (primary key of employee table) and accountId (primary key of account table). In this annotation name attribute is mandatory, rest are optional. So, if we write only

    @JoinTable(name="employee_account")
    private Account account;

    then still it will work. It will create column names as account_aid(concatenation of child table name, underscore and primary key of its table) & eid(primary key of the owner entity).

    If we want bidirectional association then in the child entity we can use either @OneToOne(mappedBy = "account") or @JoinTable with @OneToOne. If we want to use @JoinTable then we must use @OneToOne with or without mappedBy. If we only use @JoinTable without @OneToOne then one extra column employee will be created in the account table with empty value.

      public class Account {
         private int aid;
         private String accountNo;
         private String branch;
         @OneToOne(mappedBy = "account")
         // @JoinTable(name = "employee_account", joinColumns = @JoinColumn(name = "accountId"), inverseJoinColumns = @JoinColumn(name = "employeeId"))
         private Employee employee;
      }

    If we don't write mappedBy attribute then Account entity will create an extra column employee_eid as foreign key and store Employee's primary key.

      Hibernate: create table Account (aid integer not null auto_increment, accountNo varchar(255), branch varchar(255), employee_eid integer, primary key (aid)) engine=MyISAM
    
      Hibernate: alter table Account add constraint FKejw579y4swv5plx8wfxh4bxk5 foreign key (employee_eid) references Employee (eid)

    Using a Shared Primary Key: In this technique, Hibernate will ensure that it will use a common primary key value in both tables. This way primary key of Employee Entity can safely be assumed the primary key of Account Entity also. In this approach, @PrimaryKeyJoinColumn is the main annotation to be used in the owner's entity.

      @Table
      @Entity
      public class Employee  implements Serializable{
         @Id
         @GeneratedValue(strategy = GenerationType.IDENTITY)
         private int eid;
         private String firstName;
         private String lastName;
         private double salary;
         @OneToOne(cascade = CascadeType.PERSIST)
         @PrimaryKeyJoinColumn
         private Account account;
      }
      @Table
      @Entity
      public class Account implements Serializable{
         @Id
         @GeneratedValue(strategy = GenerationType.IDENTITY)
         private int aid;
         private String accountNo;
         private String branch;
      }
      Hibernate: create table Account (aid integer not null auto_increment, accountNo varchar(255), branch varchar(255), primary key (aid)) engine=MyISAM
    
      Hibernate: create table Employee (eid integer not null auto_increment, firstName varchar(255), lastName varchar(255), salary float(53) not null, primary key (eid)) engine=MyISAM
    
      Hibernate: insert into Account (accountNo, branch) values (?, ?)
    
      Hibernate: insert into Employee (firstName, lastName, salary) values (?, ?, ?)

    If we want bidirectional association then in the child entity we need to use @OneToOne(mappedBy = "account").

      public class Account {
         private int aid;
         private String accountNo;
         private String branch;
         @OneToOne(mappedBy = "account")
         private Employee employee;
      }
      Employee employee = new Employee();
      employee.setFirstName("David");
      employee.setLastName("Warner");
      employee.setSalary(56789);
      Account account = new Account();
      account.setAccountNo("ACC123");
      account.setBranch("Australia");
      employee.setAccount(account); // this is enough for bidirectional association
      //	account.setEmployee(employee); // this line is optional
      session.persist(employee);

    If we don't write mappedBy attribute then Account entity will create an extra column employee_eid as foreign key and store Employee's primary key.

      Hibernate: create table Account (aid integer not null auto_increment, accountNo varchar(255), branch varchar(255), employee_eid integer, primary key (aid)) engine=MyISAM
    
      Hibernate: alter table Account add constraint FKejw579y4swv5plx8wfxh4bxk5 foreign key (employee_eid) references Employee (eid)

    Using a Shared Primary Key: In this technique, Hibernate assumes both the source and target share the same primary key values. For that we use @MapsId annotation. The parent-side association becomes redundant(its primary key will no longer be needed).

      @Table
      @Entity
      public class Employee  implements Serializable{
         @Id
         @GeneratedValue(strategy = GenerationType.IDENTITY)
         private int eid;
         private String firstName;
         private String lastName;
         private double salary;
         @OneToOne(cascade = CascadeType.PERSIST)
         @MapsId
         private Account account;
      }
      @Table
      @Entity
      public class Account implements Serializable{
         @Id
         @GeneratedValue(strategy = GenerationType.IDENTITY)
         private int aid;
         private String accountNo;
         private String branch;
         @OneToOne(mappedBy = "account")
         private Employee employee;
      }
      Hibernate: create table Account (aid integer not null auto_increment, accountNo varchar(255), branch varchar(255), primary key (aid)) engine=MyISAM
    
      Hibernate: create table Employee (firstName varchar(255), lastName varchar(255), salary float(53) not null, account_aid integer not null, primary key (account_aid)) engine=MyISAM
    
      Hibernate: alter table Employee add constraint FKf5cbit5cfn86kiuergvpbidcr foreign key (account_aid) references Account (aid)
    
      Hibernate: insert into Account (accountNo, branch) values (?, ?)
    
      Hibernate: insert into Employee (firstName, lastName, salary, account_aid) values (?, ?, ?, ?)

    Note: For bidirectional association any of the technique it is must to write @OneToOne annotation either with or without mappedBy attribute. If not then we shall get exception:

    Exception in thread "main" jakarta.persistence.PersistenceException: Converting `org.hibernate.exception.DataException` to JPA `PersistenceException` : could not execute statement
    
    Caused by: com.mysql.jdbc.MysqlDataTruncation: Data truncation: Data too long for column 'employee' at row 1
  2. @OneToMany

    We can implement one to many association in 2 ways.

    1. Using foreign key column.
    2. Using link table.

    Using foreign key column: In this approach child table(Account) column will refer to the primary key of parent table(Employee).

      @Table
      @Entity
      public class Employee implements Serializable {
         @Id
         @GeneratedValue(strategy = GenerationType.IDENTITY)
         private int eid;
         private String firstName;
         private String lastName;
         private double salary;
         @OneToMany(cascade = CascadeType.PERSIST, mappedBy = "employee")
         private List<Account> accounts;
      }
      @Entity
      @Table
      public class Account implements Serializable {
         @Id
         @GeneratedValue(strategy = GenerationType.IDENTITY)
         private int aid;
         private String accountNo;
         private String branch;
         @ManyToOne
         @JoinColumn(name = "employeeId")
      	private Employee employee;
      }
      Employee employee = new Employee();
      employee.setFirstName("David");
      employee.setLastName("Warner");
      employee.setSalary(56789);
      Account account = new Account();
      account.setAccountNo("ACC123");
      account.setBranch("Australia");
      account.setEmployee(employee);
      List<Account> accounts = new ArrayList<>();
      accounts.add(account);
      account = new Account();
      account.setAccountNo("BCC567");
      account.setBranch("New Zealand");
      accounts.add(account);
      employee.setAccounts(accounts);
      account.setEmployee(employee);
    
      Transaction transaction = session.beginTransaction();
      session.persist(employee);
      transaction.commit();
    
      Employee employee2 = session.get(Employee.class, 1);
      Hibernate: create table Account (aid integer not null auto_increment, accountNo varchar(255), branch varchar(255), employeeId integer, primary key (aid)) engine=MyISAM
    
      Hibernate: create table Employee (eid integer not null auto_increment, firstName varchar(255), lastName varchar(255), salary float(53) not null, primary key (eid)) engine=MyISAM
    
      Hibernate: alter table Account add constraint FKbkyxfxqy2qresbaghdbm5xtty foreign key (employeeId) references Employee (eid)
    
      Hibernate: insert into Employee (firstName, lastName, salary) values (?, ?, ?)
    
      Hibernate: insert into Account (accountNo, branch, employeeId) values (?, ?, ?)
    
      Hibernate: insert into Account (accountNo, branch, employeeId) values (?, ?, ?)
    
      -: SELECT QUERIES :-
    
      Hibernate: select e1_0.eid,e1_0.firstName,e1_0.lastName,e1_0.salary from Employee e1_0 where e1_0.eid=?
    
      Hibernate: select a1_0.employeeId,a1_0.aid,a1_0.accountNo,a1_0.branch from Account a1_0 where a1_0.employeeId=?

    Using link table/Join Table: This approach uses the @JoinTable annotation to create a link table that stores the foreign keys of both the tables.

      @Entity
      public class Employee implements Serializable {
         @Id
         @GeneratedValue(strategy = GenerationType.IDENTITY)
         private int eid;
         private String firstName;
         private String lastName;
         private double salary;
         @OneToMany(cascade = CascadeType.PERSIST)
         @JoinTable(name = "empacc", joinColumns = @JoinColumn(name = "employeeId"), inverseJoinColumns = @JoinColumn(name = "accountId"))
         private List<Account> accounts;
      }
      @Entity
      public class Account implements Serializable {
         @Id
         @GeneratedValue(strategy = GenerationType.IDENTITY)
         private int aid;
         private String accountNo;
         private String branch;
         @ManyToOne
         @JoinTable(name = "empacc", joinColumns = @JoinColumn(name = "accountId"), inverseJoinColumns = @JoinColumn(name = "employeeId"))
         private Employee employee;
      }
      Employee employee = new Employee();
      employee.setFirstName("David");
      employee.setLastName("Warner");
      employee.setSalary(56789);
      Account account = new Account();
      account.setAccountNo("ACC123");
      account.setBranch("Australia");
      List<Account> accounts = new ArrayList<>();
      accounts.add(account);
      account = new Account();
      account.setAccountNo("BCC567");
      account.setBranch("New Zealand");
      accounts.add(account);
      employee.setAccounts(accounts);
    
      Transaction transaction = session.beginTransaction();
      session.persist(employee);
      transaction.commit();
      Hibernate: create table Account (aid integer not null auto_increment, accountNo varchar(255), branch varchar(255), primary key (aid)) engine=MyISAM
    
      Hibernate: create table empacc (employeeId integer, accountId integer not null, primary key (accountId)) engine=MyISAM
    
      Hibernate: create table Employee (eid integer not null auto_increment, firstName varchar(255), lastName varchar(255), salary float(53) not null, primary key (eid)) engine=MyISAM
    
      Hibernate: alter table empacc add constraint FKp2jrrkjsg7pyp97t7xn0480yj foreign key (employeeId) references Employee (eid)
    
      Hibernate: alter table empacc add constraint FK897b88v7cuvxmstxhqmad0dbe foreign key (accountId) references Account (aid)
    
      Hibernate: insert into Employee (firstName, lastName, salary) values (?, ?, ?)
    
      Hibernate: insert into Account (accountNo, branch) values (?, ?)
    
      Hibernate: insert into Account (accountNo, branch) values (?, ?)
    
      Hibernate: insert into empacc (employeeId, accountId) values (?, ?)
    
      Hibernate: insert into empacc (employeeId, accountId) values (?, ?)
  3. @ManyToMany

    A many-to-many association is made between two entities where many entities can be associated with multiple other entities. For example, for a subscription service, Subscription Entity and Reader Entity can be two types of entities. A given subscription can have multiple readers, whereas a reader can subscribe to multiple subscriptions. Many to many association requires a link table that joins two entities. It stores the foreign key of both the tables. This annotation doesn't require @JoinTable. @ManyToMany annotation is enough to create the link table.

    @Entity
    public class Reader implements Serializable {
      @Id
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      private int rid;
      private String readerName;
      private String email;
      @ManyToMany(cascade = CascadeType.PERSIST)
      private List<Subscription> subscriptions;
    }
    @Entity
    public class Subscription implements Serializable {
      @Id
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      private int sid;
      private String subscriptionName;
    }
    Subscription subscription1 = new Subscription();
    subscription1.setSubscriptionName("Youtube");
    Subscription subscription2 = new Subscription();
    subscription2.setSubscriptionName("Netflix");
    List<Subscription> subscriptions = Arrays.asList(subscription1, subscription2);
    Reader reader = new Reader();
    reader.setEmail("abc@gmail.com");
    reader.setReaderName("David");
    reader.setSubscriptions(subscriptions);
    
    Transaction transaction = session.beginTransaction();
    session.persist(reader);
    transaction.commit();
      Hibernate: create table Reader (rid integer not null auto_increment, email varchar(255), readerName varchar(255), primary key (rid)) engine=MyISAM
    
      Hibernate: create table Reader_Subscription (Reader_rid integer not null, subscriptions_sid integer not null) engine=MyISAM
      Hibernate: create table Subscription (sid integer not null auto_increment, subscriptionName varchar(255), primary key (sid)) engine=MyISAM
    
      Hibernate: alter table Reader_Subscription add constraint FKs066tyf8j84vwtv387hbboow9 foreign key (subscriptions_sid) references Subscription (sid)
    
      Hibernate: alter table Reader_Subscription add constraint FKld5v1st16r4u53vufetlbodh5 foreign key (Reader_rid) references Reader (rid)
    
      Hibernate: insert into Reader (email, readerName) values (?, ?)
    
      Hibernate: insert into Subscription (subscriptionName) values (?)
    
      Hibernate: insert into Subscription (subscriptionName) values (?)
    
      Hibernate: insert into Reader_Subscription (Reader_rid, subscriptions_sid) values (?, ?)
    
      Hibernate: insert into Reader_Subscription (Reader_rid, subscriptions_sid) values (?, ?)

    If we want bidirectional association then in the child class we need to use @ManyToMany with mappedBy attribute. Rest everything will be same. Even generated SQL query will be same.

    @Entity
    public class Subscription implements Serializable {
      @Id
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      private int sid;
      private String subscriptionName;
      @ManyToMany(mappedBy = "subscriptions")
      List<Reader> readers;
    }
  4. @ManyToOne

    A many-to-one association is made between two entities where many entities can be associated with one entity. For example, Many students work on a single project. In this association, many side will be the owner and creates a foreign key column in its table.

    @Entity
    public class Student implements Serializable {
      @Id
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      private int sid;
      private String name;
      @ManyToOne(cascade = CascadeType.PERSIST)
      @JoinColumn(name = "projectId")
      private Project project;
    }
    @Entity
    @Data
    @ToString(exclude = "students")
    public class Project implements Serializable {
      @Id
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      private int pid;
      private String projectName;
      @OneToMany(mappedBy = "project")
      private List<Student> students;
    }
    Transaction transaction = session.beginTransaction();
    Project project = new Project();
    project.setProjectName("HMS");
    
    Student student1 = new Student();
    student1.setName("Ajay");
    student1.setProject(project);
    session.persist(student1);
    
    Student student2 = new Student();
    student2.setName("David");
    student2.setProject(project);
    session.persist(student2);
    transaction.commit();
    Hibernate: create table Project (pid integer not null auto_increment, projectName varchar(255), primary key (pid)) engine=MyISAM
    
    Hibernate: create table Student (sid integer not null auto_increment, name varchar(255), projectId integer, primary key (sid)) engine=MyISAM
    
    Hibernate: alter table Student add constraint FKb3wn40e1o66egelusymas8fvo foreign key (projectId) references Project (pid)
    
    Hibernate: insert into Project (projectName) values (?)
    
    Hibernate: insert into Student (name, projectId) values (?, ?)
    
    Hibernate: insert into Student (name, projectId) values (?, ?)
  5. @Temporal

    By default, java.util.Date & java.util.Calendar will be stored in a column with the TIMESTAMP data type, but this default behavior can be overridden with the @Temporal annotation. This annotation accepts a single value attribute from the jakarta.persistence.TemporalType enum. This offers 3 possible values: DATE, TIME, and TIMESTAMP. These correspond, respectively to java.sql.Date, java.sql.Time and java.sql.Timestamp. The table column is given the appropriate data type at the time of schema generation.

    @Entity
    public class Article {
      @Id
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      private int id;
      private String title;
      private String content;
      @Temporal(TemporalType.DATE)
      private Date date;
      @Temporal(TemporalType.TIME)
      private Calendar calendar;
      private LocalDate updatedDate;
      private LocalTime updatedTime;
      private LocalDateTime publishedTimestamp;
    }

    Since Java 8 has introduced new date/time API that can be mapped directly to SQL types. We don't need to use @Temporal annotation.

    1. java.time.LocalDate can be converted into sql DATE type.
    2. java.time.LocalTime can be converted into sql TIME type.
    3. java.time.LocalDateTime can be converted into sql TIMESTAMP type.
  6. @ElementCollection

    This annotation is almost same as @OneToMany. It creates a a foreign key in the child side and stores the primary key of the owner entity but the difference is that the child side should be non-mapping class Embeddable or Basic. The child side should not be an Entity. The elements of child side are completely owned by the containing entities(Owner). They are modified when the owner entity is modified, deleted when the owner entity is deleted, etc. They can't have their own lifecycle.

    @Entity
    public class State {
      @Id
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      private int id;
      private String name;
      @ElementCollection
      private List<String> cities;
    }
    State state = new State();
    state.setName("Telangana");
    List<String> cities = List.of("Hyderabad", "Secundarabad", "Nalgonda");
    state.setCities(cities);
    Transaction transaction = session.beginTransaction();
    session.persist(state);
    transaction.commit();
    Hibernate: create table State (id integer not null auto_increment, name varchar(255), primary key (id)) engine=MyISAM
    Hibernate: create table State_cities (State_id integer not null, cities varchar(255)) engine=MyISAM
    Hibernate: alter table State_cities add constraint FK8dvxj5lk3fvf14fclwj7ber1p foreign key (State_id) references State (id)
    Hibernate: insert into State (name) values (?)
    Hibernate: insert into State_cities (State_id, cities) values (?, ?)
    Hibernate: insert into State_cities (State_id, cities) values (?, ?)
    Hibernate: insert into State_cities (State_id, cities) values (?, ?)
    @Entity
    public class Employee {
      @Id
      private long id;
      @ElementCollection
      private List<Phone> phones;
    }
    
    @Embeddable
    public class Phone {
      private String type;
      private String areaCode;
      private String number;
    }
  7. @Lob

    A persistent property or field can be marked for persistence as a database-supported large object type by applying the @Lob annotation. The annotation takes no attributes, but the underlying large object type to be used will be inferred from the type of the field or parameter. String- and character-based types will be stored in an appropriate character-based type i.e. CLOB. All other objects will be stored in a BLOB. This annotation can be used in combination with the @Basic or the @ElementCollection annotation.

      @Entity
      @Data
      public class Article {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private int id;
        private String title;
        @Lob
        private String content;
      }
    
      Transaction transaction = session.beginTransaction();
      Article article = new Article();
      article.setTitle("Title");
      article.setContent("A very long article content");
      session.persist(article);
      transaction.commit();
    Hibernate: create table Article (id integer not null auto_increment, content tinytext, title varchar(255), primary key (id)) engine=MyISAM
    
    Hibernate: insert into Article (content, title) values (?, ?)
  8. @OrderBy

    This annotation specifies the ordering of the elements of a collection valued association or element collection at the point when the association or collection is retrieved from database.

    @Entity
    public class Course {
      @Id
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      private int id;
      private String courseName;
      @ManyToMany(cascade = CascadeType.ALL)
      @OrderBy("name")
      private List<Student> students;
    }

    When we fetch list of students from database then it uses SELECT query with order by clause.

    @Entity
    public class Student {
      @Id
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      private int id;
      private String name;
      @ManyToMany(mappedBy = "students")
      private List<Course> courses;
    }
    Student student1 = new Student();
    student1.setName("David");
    Student student2 = new Student();
    student2.setName("Zahid");
    Student student3 = new Student();
    student3.setName("Ajay");
    Course course = new Course();
    course.setCourseName("Java");
    course.setStudents(List.of(student1, student2, student3));
    Transaction transaction = session.beginTransaction();
    session.persist(course);
    transaction.commit();
    Hibernate: create table Course (id integer not null auto_increment, courseName varchar(255), primary key (id)) engine=MyISAM
    
    Hibernate: create table Course_Student (courses_id integer not null, students_id integer not null) engine=MyISAM
    
    Hibernate: create table Student (id integer not null auto_increment, name varchar(255), primary key (id)) engine=MyISAM
    
    Hibernate: alter table Course_Student add constraint FKhsjbkfb9v2y4f2h02q14ft5pd foreign key (students_id) references Student (id)
    
    Hibernate: alter table Course_Student add constraint FKs7yq2swrenbdpvkwx01j9cg1s foreign key (courses_id) references Course (id)
    
    Hibernate: insert into Course (courseName) values (?)
    Hibernate: insert into Student (name) values (?)
    Hibernate: insert into Student (name) values (?)
    Hibernate: insert into Student (name) values (?)
    Hibernate: insert into Course_Student (courses_id, students_id) values (?, ?)
    Hibernate: insert into Course_Student (courses_id, students_id) values (?, ?)
    Hibernate: insert into Course_Student (courses_id, students_id) values (?, ?)
    Course course2 = session.get(Course.class, 1);
    if (course2 == null) {
      System.out.println(null);
    } else {
      System.out.println(course2);
      List<Student> students = course2.getStudents();
      for (Student student : students) {
        System.out.println(student);
      }
    }
    Hibernate: select c1_0.id,c1_0.courseName from Course c1_0 where c1_0.id=?
    
    Hibernate: select s1_0.courses_id,s1_1.id,s1_1.name from Course_Student s1_0 join Student s1_1 on s1_1.id=s1_0.students_id where s1_0.courses_id=? order by s1_1.name
    
    Course(id=1, courseName=Java, students=[Student(id=3, name=Ajay), Student(id=1, name=David), Student(id=2, name=Zahid)])
    Student(id=3, name=Ajay)
    Student(id=1, name=David)
    Student(id=2, name=Zahid)

    If ASC or DESC is not specified, ASC (ascending order) is assumed. If the ordering element is not specified for an entity association, ordering by the primary key of the associated entity is assumed. The property or field name must correspond to that of a persistent property or field of the associated class or embedded class within it. The properties or fields used in the ordering must correspond to columns for which comparison operators are supported. The @OrderBy annotation is not used when an @OrderColumn is specified.

    The dot (".") notation is used to refer to an attribute within an embedded attribute. The value of each identifier used with the dot notation is the name of the respective embedded field or property.

    @Embeddable
    public class Zipcode {
      protected String zip;
      protected int areaCode;
    }
    
    @Embeddable
    public class Address {
      protected String street;
      protected String city;
      protected String state;
      @Embedded
      protected Zipcode zipcode;
    }
    
    @Entity
    public class Person {
      @Id
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      private int id;
      private String name;
      @ElementCollection
      @OrderBy("zipcode.zip DESC")
      public Set<Address> addresses;
    }
    Zipcode zipcode1 = new Zipcode();
    zipcode1.setAreaCode(51);
    zipcode1.setZip("765478");
    Address address1 = new Address();
    address1.setCity("Hyderabad");
    address1.setState("Telangana");
    address1.setStreet("Tiger chowk");
    address1.setZipcode(zipcode1);
    
    Zipcode zipcode2 = new Zipcode();
    zipcode2.setAreaCode(24);
    zipcode2.setZip("123456");
    Address address2 = new Address();
    address2.setCity("Bengaluru");
    address2.setState("Karnataka");
    address2.setStreet("Howra bridge");
    address2.setZipcode(zipcode2);
    
    Person person = new Person();
    person.setName("Ajay");
    person.setAddresses(Set.of(address1, address2, address3));
    Transaction transaction = session.beginTransaction();
    session.persist(person);
    transaction.commit();
    Hibernate: create table Person (id integer not null auto_increment, name varchar(255), primary key (id)) engine=MyISAM
    
    Hibernate: create table Person_addresses (Person_id integer not null, city varchar(255), state varchar(255), street varchar(255), areaCode integer not null, zip varchar(255)) engine=MyISAM
    
    Hibernate: alter table Person_addresses add constraint FKbs25rlvs45qpe83fidi1x1nj0 foreign key (Person_id) references Person (id)
    
    Hibernate: insert into Person (name) values (?)
    Hibernate: insert into Person_addresses (Person_id, city, state, street, areaCode, zip) values (?, ?, ?, ?, ?, ?)
    Hibernate: insert into Person_addresses (Person_id, city, state, street, areaCode, zip) values (?, ?, ?, ?, ?, ?)
    Person person2 = session.get(Person.class, 1);
    System.out.println(person2);
    Hibernate: select p1_0.id,p1_0.name from Person p1_0 where p1_0.id=?
    
    Hibernate: select a1_0.Person_id,a1_0.city,a1_0.state,a1_0.street,a1_0.areaCode,a1_0.zip from Person_addresses a1_0 where a1_0.Person_id=? order by a1_0.zip
    
    Person(id=1, name=Ajay, addresses=[Address(street=Tiger chowk, city=Hyderabad, state=Telangana, zipcode=Zipcode(zip=765478, areaCode=51)), Address(street=Howra bridge, city=Bengaluru, state=Karnataka, zipcode=Zipcode(zip=123456, areaCode=24))])
  9. @OrderColumn

    This annotation defines an additional column in the child table(many side) which stores the order of the items from zero onwards. For each parent record it starts from zero.

    @Entity
    public class CreditCard {
      @Id
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      private int cid;
      private String name;
      private String number;
      @OneToMany(cascade = CascadeType.ALL, mappedBy = "creditCard")
      @OrderColumn(name = "transactionsOrder")
      List<CardTransaction> cardTransactions;
    }
    
    @Entity
    public class CardTransaction {
      @Id
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      private int tid;
      private double amount;
      private String date;
      @ManyToOne
      @JoinColumn(name = "creditCardId")
      private CreditCard creditCard;
    }
    CreditCard creditCard = new CreditCard();
    creditCard.setName("Samar");
    creditCard.setNumber("1234567890123456");
    CardTransaction cardTransaction1 = new CardTransaction();
    cardTransaction1.setAmount(567);
    cardTransaction1.setDate("2022-11-25");
    cardTransaction1.setCreditCard(creditCard);
    CardTransaction cardTransaction2 = new CardTransaction();
    cardTransaction2.setAmount(984);
    cardTransaction2.setDate("2021-04-18");
    cardTransaction2.setCreditCard(creditCard);
    creditCard.setCardTransactions(List.of(cardTransaction1, cardTransaction2));
    
    Transaction transaction = session.beginTransaction();
    session.persist(creditCard);
    transaction.commit();
    Hibernate: create table CardTransaction (tid integer not null auto_increment, amount float(53) not null, date varchar(255), creditCardId integer, transactionsOrder integer, primary key (tid)) engine=MyISAM
    
    Hibernate: create table CreditCard (cid integer not null auto_increment, name varchar(255), number varchar(255), primary key (cid)) engine=MyISAM
    
    Hibernate: alter table CardTransaction add constraint FK4uq1rd0tljxhx811rppbo39t9 foreign key (creditCardId) references CreditCard (cid)
    
    Hibernate: insert into CreditCard (name, number) values (?, ?)
    
    Hibernate: insert into CardTransaction (amount, creditCardId, date) values (?, ?, ?)
    
    Hibernate: insert into CardTransaction (amount, creditCardId, date) values (?, ?, ?)
    
    Hibernate: update CardTransaction set transactionsOrder=? where tid=?
    
    Hibernate: update CardTransaction set transactionsOrder=? where tid=?

    So it will store 0 in the 1st row & 1 in the 2nd row of the cardtransaction table and in the column transactionsOrder. And accordingly it will fetch the data.

    Hibernate: select c1_0.cid,c1_0.name,c1_0.number from CreditCard c1_0 where c1_0.cid=?
    
    Hibernate: select c1_0.creditCardId,c1_0.transactionsOrder,c1_0.tid,c1_0.amount,c1_0.date from CardTransaction c1_0 where c1_0.creditCardId=?

    By default, the column can contain null (unordered) values. The nullability can be overridden by setting the nullable attribute to false. By default, when the schema is generated from the annotations, the column is assumed to be an integer type; however, this can be overridden by supplying a columnDefinition attribute specifying a different column definition string.

  10. CascadeType

    When an entity is associated with another entity in any type of association(1:1, 1:N, N:1, N:N) then any change in the owner entity will reflect to the child entity according to the cascade type. If we save an Employee then all associated Accounts will also be saved into database. If we delete an Employee then all Accounts associated with that Employee also be deleted.

    @OneToMany(cascade = CascadeType.PERSIST, mappedBy = "employee")
    private List<Account> accounts;

    JPA provides various cascade types like CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH, CascadeType.REMOVE, CascadeType.DETATCH, CascadeType.ALL. There is no default cascade type in JPA. By default no operation is cascaded. The cascade configuration option accepts an array of CascadeType. Thus to include more than one cascade type we can use like:

    @OneToMany(cascade={CascadeType.PERSIST, CascadeType.MERGE}, mappedBy = "employee")
    private List<Account> accounts;
  11. CascadeType.REMOVE vs Orphan Removal

    CascadeType.REMOVE is a database-specific thing. This is a way to delete a child entity or entities whenever the deletion of its parent happens.

    public class Employee implements Serializable {
      @Id
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      private int eid;
      private String firstName;
      private String lastName;
      private double salary;
      @OneToMany(cascade = { CascadeType.REMOVE, CascadeType.PERSIST }, mappedBy = "employee")
      private List<Account> accounts;
    }
    
    public class Account implements Serializable {
      @Id
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      private int aid;
      private String accountNo;
      private String branch;
      @ManyToOne(cascade = CascadeType.PERSIST)
      @JoinColumn(name = "employeeId")
      private Employee employee;
    }
    Employee employee = new Employee();
    employee.setFirstName("David");
    employee.setLastName("Warner");
    employee.setSalary(56789);
    Account account = new Account();
    account.setAccountNo("ACC123");
    account.setBranch("Australia");
    account.setEmployee(employee);
    List<Account> accounts = new ArrayList<>();
    accounts.add(account);
    account = new Account();
    account.setAccountNo("BCC567");
    account.setBranch("New Zealand");
    accounts.add(account);
    employee.setAccounts(accounts);
    account.setEmployee(employee);
    
    Session session = sessionFactory.openSession();
    Transaction transaction = session.beginTransaction();
    session.persist(employee);
    transaction.commit();
    session.close();
    
    Session session2 = sessionFactory.openSession();
    Transaction transaction2 = session2.beginTransaction();
    Employee employee2 = session2.get(Employee.class, 1);
    session2.remove(employee2);
    transaction2.commit();
    session2.close();
    Hibernate: create table Account (aid integer not null auto_increment, accountNo varchar(255), branch varchar(255), employeeId integer, primary key (aid)) engine=MyISAM
    
    Hibernate: create table Employee (eid integer not null auto_increment, firstName varchar(255), lastName varchar(255), salary float(53) not null, primary key (eid)) engine=MyISAM
    
    Hibernate: alter table Account add constraint FKbkyxfxqy2qresbaghdbm5xtty foreign key (employeeId) references Employee (eid)
    
    Hibernate: insert into Employee (firstName, lastName, salary) values (?, ?, ?)
    Hibernate: insert into Account (accountNo, branch, employeeId) values (?, ?, ?)
    Hibernate: insert into Account (accountNo, branch, employeeId) values (?, ?, ?)
    
    Hibernate: select e1_0.eid,e1_0.firstName,e1_0.lastName,e1_0.salary from Employee e1_0 where e1_0.eid=?
    
    Hibernate: select a1_0.employeeId,a1_0.aid,a1_0.accountNo,a1_0.branch from Account a1_0 where a1_0.employeeId=?
    
    Hibernate: delete from Account where aid=?
    Hibernate: delete from Account where aid=?
    Hibernate: delete from Employee where eid=?

    The orphanRemoval=true option was introduced in JPA 2.0. This marks child entity to be removed when it's no longer referenced from the parent entity. e.g. when we remove the child entity from the corresponding collection of the parent entity.

    public class Employee implements Serializable {
      @Id
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      private int eid;
      private String firstName;
      private String lastName;
      private double salary;
      @OneToMany(cascade = CascadeType.PERSIST, mappedBy = "employee", orphanRemoval = true)
      private List<Account> accounts;
    }
    
    public class Account implements Serializable {
      @Id
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      private int aid;
      private String accountNo;
      private String branch;
      @ManyToOne
      @JoinColumn(name = "employeeId")
      private Employee employee;
    }
    Employee employee = new Employee();
    employee.setFirstName("David");
    employee.setLastName("Warner");
    employee.setSalary(56789);
    Account account = new Account();
    account.setAccountNo("ACC123");
    account.setBranch("Australia");
    account.setEmployee(employee);
    List<Account> accounts = new ArrayList<>();
    accounts.add(account);
    account = new Account();
    account.setAccountNo("BCC567");
    account.setBranch("New Zealand");
    accounts.add(account);
    employee.setAccounts(accounts);
    account.setEmployee(employee);
    
    Session session1 = sessionFactory.openSession();
    Transaction transaction1 = session1.beginTransaction();
    session1.persist(employee);
    transaction1.commit();
    session1.close();
    
    Session session2 = sessionFactory.openSession();
    Transaction transaction2 = session2.beginTransaction();
    Employee employee2 = session2.get(Employee.class, 1);
    List<Account> accounts = employee2.getAccounts();
    accounts.remove(0); // remove an account from List of accounts, not from session
    transaction2.commit();
    session2.close();
    Hibernate: create table Account (aid integer not null auto_increment, accountNo varchar(255), branch varchar(255), employeeId integer, primary key (aid)) engine=MyISAM
    
    Hibernate: create table Employee (eid integer not null auto_increment, firstName varchar(255), lastName varchar(255), salary float(53) not null, primary key (eid)) engine=MyISAM
    
    Hibernate: alter table Account add constraint FKbkyxfxqy2qresbaghdbm5xtty foreign key (employeeId) references Employee (eid)
    
    Hibernate: insert into Employee (firstName, lastName, salary) values (?, ?, ?)
    Hibernate: insert into Account (accountNo, branch, employeeId) values (?, ?, ?)
    Hibernate: insert into Account (accountNo, branch, employeeId) values (?, ?, ?)
    
    Hibernate: select e1_0.eid,e1_0.firstName,e1_0.lastName,e1_0.salary from Employee e1_0 where e1_0.eid=?
    
    Hibernate: select a1_0.employeeId,a1_0.aid,a1_0.accountNo,a1_0.branch from Account a1_0 where a1_0.employeeId=?
    
    Hibernate: delete from Account where aid=?
  12. HQL

    HQL is an object-oriented query language, similar to SQL, but instead of operating on tables and columns, HQL works with persistent/entity objects and their properties. HQL is a superset of the JPQL, the Java Persistence Query Language. A JPQL query is a valid HQL query, but not all HQL queries are valid JPQL queries. HQL is a language with its own syntax and grammar. It is written as strings, like "from Product p". HQL queries are translated by Hibernate into conventional SQL queries. Note that Hibernate also provides the APIs that allow us to directly issue SQL queries as well. Hibernator’s query facilities do not allow us to alter the database schema. We can only add/update/delete the data inside tables. HQL is in case-sensitive except the class name and its attributes.

    HQL UPDATE: UPDATE alters the details of existing objects in the database.

    	Session session = sessionFactory.openSession();
    	Transaction transaction = session.beginTransaction();
    	Employee employee = new Employee();
    	employee.setFirstName("David");
    	employee.setLastName("Warner");
    	employee.setSalary(56789);
    	session.persist(employee);
    	transaction.commit();
    	session.close();
    
    	Session session2 = sessionFactory.openSession();
    	Transaction transaction2 = session2.beginTransaction();
    	Query query = session2.createQuery("UPDATE Employee set firstName=:name where eid=:id");
    	query.setParameter("name", "Finch");
    	query.setParameter("id", 1);
    	int noOfRowsAffected = query.executeUpdate();
    	transaction2.commit();
    	session2.close();

    HQL DELETE: DELETE removes the details of existing objects from the database.

    Session session = sessionFactory.openSession();
    Transaction transaction = session.beginTransaction();
    for (int i = 1; i <= 2; i++) {
      Employee employee = new Employee();
      employee.setFirstName("David" + i);
      employee.setLastName("Warner" + i);
      employee.setSalary(1000 * i);
      session.persist(employee);
    }
    transaction.commit();
    session.close();
    
    Session session2 = sessionFactory.openSession();
    Transaction transaction2 = session2.beginTransaction();
    String qry = "deLETE from Employee where eid=:id";	// query is in case-sensitive
    Query query = session2.createQuery(qry);
    query.setParameter("id", 1);
    int noOfRowsAffected = query.executeUpdate();
    transaction2.commit();
    session2.close();
    Hibernate: create table Employee (eid integer not null auto_increment, firstName varchar(255), lastName varchar(255), salary float(53) not null, primary key (eid)) engine=MyISAM
    
    Hibernate: insert into Employee (firstName, lastName, salary) values (?, ?, ?)
    Hibernate: insert into Employee (firstName, lastName, salary) values (?, ?, ?)
    
    Hibernate: delete from Employee where eid=?

    HQL INSERT: An HQL INSERT can be used to directly insert arbitrary entities as well as insert entities constructed from information obtained from SELECT queries.

    public class Person implements Serializable {
      @Id
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      private int id;
      private String firstName;
      private String lastName;
    }
    
    Transaction transaction = session.beginTransaction();
    String qry = "insert into Person(firstName, lastName)" + "values(:firstName, :lastName)";
    Query query = session.createQuery(qry);
    query.setParameter("firstName", "Moin");
    query.setParameter("lastName", "Ali");
    int noOfRowsAffected = query.executeUpdate();
    transaction.commit();
    System.out.println("noOfRowsAffected: " + noOfRowsAffected);
    Hibernate: create table Person (id integer not null auto_increment, firstName varchar(255), lastName varchar(255), primary key (id)) engine=MyISAM
    
    Hibernate: insert into Person(firstName,lastName) values (?,?)

    Insert with Select query:

    Query query=session.createQuery("insert into purged_accounts(id, code, status) "+
    "select id, code, status from account where accStatus=:status");
    
    query.setString("accStatus", "PURGED");
    int rowsCopied=query.executeUpdate();

    HQL SELECT: An HQL SELECT is used to query the database for classes and their properties.

    [SELECT [DISTINCT] property [, ...]]
      FROM path [[AS] alias] [, ...] [FETCH ALL PROPERTIES]
      WHERE logicalExpression
      GROUP BY property [, ...]
      HAVING logicalExpression
      ORDER BY property [ASC | DESC] [, ...]

    If FETCH ALL PROPERTIES is used then lazy loading semantics will be ignored, and all the immediate properties of the retrieved object(s) will be actively loaded (this does not apply recursively). SELECT clause is optional in HQL but mandatory for JPQL.

    Session session = sessionFactory.openSession();
    Query query = session.createQuery("from Employee");
    List<Employee> employees = query.getResultList();
    for (Employee employee : employees) {
      System.out.println(employee);
    }
    Hibernate: select e1_0.eid,e1_0.firstName,e1_0.lastName,e1_0.salary from Employee e1_0
    
    Employee(eid=1, firstName=David1, lastName=Warner1, salary=1000.0)
    Employee(eid=2, firstName=David2, lastName=Warner2, salary=2000.0)
    Query query = session3.createQuery("select firstName, salary from Employee");
    // Query query = session.createQuery("select firstName from Employee");
    List<Object> employeeList = query.getResultList();
    if (!employeeList.isEmpty()) {
      if (employeeList.get(0).getClass().isArray()) {
        for (Object object : employeeList) {
          for (Object cell : (Object[]) object) {
            System.out.print(cell + " ");
          }
          System.out.println();
        }
      } else {
        for (Object column : employeeList) {
          System.out.println(column);
        }
      }
    }
    Hibernate: select e1_0.firstName,e1_0.salary from Employee e1_0
    David1 1000.0
    David2 2000.0

    HQL SELECT Clause & Projections: The SELECT clause provides more control over the result set than the from clause. If we want to obtain the properties of objects in the result set, use the SELECT clause. For instance, we could run a projection query on the products in the database that only returned the names, instead of loading the full object into memory, as follows:

    select product.name from Product product

    The result set for this query will contain a List of java.lang.String objects. Additionally, we can retrieve the prices and the names for each product in the database, like so:

    select product.name, product.price from Product product

    If we are only interested in a few properties, this approach can allow us to reduce network traffic to the database server and save memory on the application's machine.

    Named Parameters: Hibernate supports named parameters in its HQL queries. This makes writing queries that accept input from the user easyily and we do not have to defend against SQL injection attacks. We can either pass index or directly the parameter name.

    String hql = "from Product where price > :productPrice";
    Query query = session.createQuery(hql);
    // query.setDouble(0, 25.0);
    query.setDouble("productPrice", 25.0);
    List results = query.list();

    Sorting the Result: To sort our HQL query results, we shall need to use the order by clause. We can order the results by any property on the objects in the result set either ascending (asc) or descending (desc).

    from Product p where p.price>25.0 order by p.price desc

    If we wanted to sort by more than one property, we would just add the additional properties to the end of the order by clause, separated by commas.

    from Product p order by p.supplier.name asc, p.price asc

    HQL Association: Associations allow us to use more than one class in an HQL query, just as SQL allows us to use joins between tables in a relational database. Hibernate supports five different types of joins:

    • CROSS JOIN
    • INNER JOIN
    • LEFT OUTER JOIN
    • RIGHT OUTER JOIN
    • FULL OUTER JOIN

    If we want to use CROSS JOIN, we just need to specify both classes in the from clause.

    from Product p, Supplier s

    For the other joins, use a join clause after the from clause. Specify the type of join, the object property to join on, and an alias for the other class.

    from Product p left outer join p.supplier as s
    
    select s.name, p.name, p.price from Product p inner join p.supplier as s

    HQL Aggregate Methods: HQL supports a range of aggregate methods, similar to SQL. They work the same way in HQL as in SQL, so we do not have to learn any specific Hibernate terminology. The difference is that in HQL aggregate methods apply to the properties of persistent objects.

    select count(*) from Product product

    The aggregate functions available through HQL include the following:

    • avg(property name): The average of a property’s value
    • count(property name or *): The number of times a property occurs in the results
    • max(property name): The maximum value of the property values
    • min(property name): The minimum value of the property values
    • sum(property name): The sum total of the property values
  13. @NamedQuery

    A named query is a static HQL or SQL query with a fixed query string and defined either using @NamedQuery annotation or an XML file. We can refer to a named query by its name, in the runtime, when we need to execute it. Note that Hibernate's @NamedQuery annotation extends JPA’s @NamedQuery annotation with some additional features. Named queries are compiled when SessionFactory is instantiated (so, essentially, when our application starts up). So the advantage is that all our named queries are validated at that time rather than failing upon execution. The other advantage is that they are easy to maintain complex queries. These can be accessed and used from several places in the application which increases re-usability.

    The disadvantage is that named queries are not customizable at runtime. We can of course define/supply parameters but beyond that what we have defined is what we shall get. We can't even change the sorting. Another disadvantage is that we shall not be able to change the named query within a running application server without reloading the SessionFactory.

    public final class Constant {
      public static final String GET_EMPLOYEE_BY_ID_NAME = "GET_EMPLOYEE_BY_ID";
      public static final String GET_EMPLOYEE_BY_ID_QUERY = "from Employee where eid=:id";
    }
    
    @Entity
    @NamedQuery(name = Constant.GET_EMPLOYEE_BY_ID_NAME, query = Constant.GET_EMPLOYEE_BY_ID_QUERY)
    public class Employee implements Serializable {
      @Id
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      private int eid;
      private String firstName;
      private String lastName;
      private double salary;
    }
    Session session = sessionFactory.openSession();
    Transaction transaction = session.beginTransaction();
    for (int i = 1; i <= 2; i++) {
      Employee employee = new Employee();
      employee.setFirstName("David" + i);
      employee.setLastName("Warner" + i);
      employee.setSalary(1000 * i);
      session.persist(employee);
    }
    transaction.commit();
    session.close();
    
    Session session2 = sessionFactory.openSession();
    Query query = session2.createNamedQuery(Constant.GET_EMPLOYEE_BY_ID_NAME);
    query.setParameter("id", 2);
    Employee employee = (Employee) query.getSingleResult();
    System.out.println(employee);
    session2.close();
    Hibernate: insert into Employee (firstName, lastName, salary) values (?, ?, ?)
    Hibernate: insert into Employee (firstName, lastName, salary) values (?, ?, ?)
    Hibernate: select e1_0.eid,e1_0.firstName,e1_0.lastName,e1_0.salary from Employee e1_0 where e1_0.eid=?
    
    Employee(eid=2, firstName=David2, lastName=Warner2, salary=2000.0)

    @NamedQueries: If we have multiple named queries for an entity we can group them using the @NamedQueries annotation.

    @NamedQueries({
        @NamedQuery(name = "QUERY_GET_DEPARTMENT_BY_ID",
            query = "from DepartmentEntity d where d.id = :id"),
        @NamedQuery(name = "QUERY_UPDATE_DEPARTMENT_BY_ID",
            query = "UPDATE DepartmentEntity d SET d.name=:name where d.id = :id")
    })

    @NamedNativeQuery: The @NamedNativeQuery works very similar to @NamedQuery except we need to write the native SQL statements instead of HQL.

    @NamedNativeQueries({
        @NamedNativeQuery(name = "NATIVE_QUERY_GET_DEPARTMENT_BY_ID",
            query = "SELECT * FROM TBL_DEPT d WHERE d.id = :id"),
        @NamedNativeQuery(name = "NATIVE_QUERY_UPDATE_DEPARTMENT_BY_ID",
            query = "UPDATE TBL_DEPT d SET d.name=:name WHERE d.id = :id")
    })
  14. Criteria Queries

    The criteria query API lets us build nested, structured query expressions in Java, providing a compile-time syntax checking. That is not possible with a query language like HQL or SQL.

    The Hibernate Criteria API had been deprecated back in Hibernate 5.x and these have been removed in Hibernate 6.0. Usually, all queries using the legacy API can be modeled with the JPA Criteria API that is still supported.

    Session session = sessionFactory.openSession();
    Transaction transaction = session.beginTransaction();
    for (int i = 1; i <= 2; i++) {
      Employee employee = new Employee();
      employee.setFirstName("David" + i);
      employee.setLastName("Warner" + i);
      employee.setSalary(1000 * i);
      session.persist(employee);
    }
    transaction.commit();
    session.close();
    
    Session session2 = sessionFactory.openSession();
    HibernateCriteriaBuilder criteriaBuilder = session2.getCriteriaBuilder();
    JpaCriteriaQuery<Employee> criteriaQuery = criteriaBuilder.createQuery(Employee.class);
    JpaRoot<Employee> from = criteriaQuery.from(Employee.class);
    JpaCriteriaQuery<Employee> select = criteriaQuery.select(from);
    Query<Employee> query = session2.createQuery(select);
    List<Employee> employees = query.list();
    for (Employee employee : employees) {
      System.out.println(employee);
    }
    session2.close();

    The above criteria code is equivalent to this HQL:

    Query query = session.createQuery("from Employee");
    Hibernate: create table Employee (eid integer not null auto_increment, firstName varchar(255), lastName varchar(255), salary float(53) not null, primary key (eid)) engine=MyISAM
    
    Hibernate: insert into Employee (firstName, lastName, salary) values (?, ?, ?)
    Hibernate: insert into Employee (firstName, lastName, salary) values (?, ?, ?)
    
    Hibernate: select e1_0.eid,e1_0.firstName,e1_0.lastName,e1_0.salary from Employee e1_0
    
    Employee(eid=1, firstName=David1, lastName=Warner1, salary=1000.0)
    Employee(eid=2, firstName=David2, lastName=Warner2, salary=2000.0)

    Restriction: We can fetch partial rows from the tables by appying restriction. We can add restriction in criteria builder.

    • criteriaBuilder.greaterThan()
    • criteriaBuilder.greaterThanOrEqualTo()
    • criteriaBuilder.lessThan
    • criteriaBuilder.lessThanOrEqualTo()
    • criteriaBuilder.equal()
    • criteriaBuilder.notEqual()
    • criteriaBuilder.between()
    • criteriaBuilder.like()
    • criteriaBuilder.notLike()
    • criteriaBuilder.isNull()
    • criteriaBuilder.isNotNull()
    Session session = sessionFactory.openSession();
    Transaction transaction = session.beginTransaction();
    for (int i = 1; i <= 2; i++) {
      Employee employee = new Employee();
      employee.setFirstName("David" + i);
      employee.setLastName("Warner" + i);
      employee.setSalary(1000 * i);
      session.persist(employee);
    }
    transaction.commit();
    session.close();
    
    Session session2 = sessionFactory.openSession();
    HibernateCriteriaBuilder criteriaBuilder = session2.getCriteriaBuilder();
    JpaCriteriaQuery<Employee> criteriaQuery = criteriaBuilder.createQuery(Employee.class);
    JpaRoot<Employee> fromEmployee = criteriaQuery.from(Employee.class);
    // criteriaQuery.where(criteriaBuilder.greaterThan(fromEmployee.get("eid"), 1));
    JpaCriteriaQuery<Employee> select = criteriaQuery.select(fromEmployee);
    select.where(criteriaBuilder.greaterThan(fromEmployee.get("eid"), 1));
    Query<Employee> query = session2.createQuery(select);
    List<Employee> employees = query.list();
    for (Employee employee : employees) {
      System.out.println(employee);
    }
    session2.close();
    Hibernate: create table Employee (eid integer not null auto_increment, firstName varchar(255), lastName varchar(255), salary float(53) not null, primary key (eid)) engine=MyISAM
    
    Hibernate: insert into Employee (firstName, lastName, salary) values (?, ?, ?)
    Hibernate: insert into Employee (firstName, lastName, salary) values (?, ?, ?)
    
    Hibernate: select e1_0.eid,e1_0.firstName,e1_0.lastName,e1_0.salary from Employee e1_0 where e1_0.eid>?
    
    Employee(eid=2, firstName=David2, lastName=Warner2, salary=2000.0)

    We can combine these restrictions like:

    JpaPredicate jpaPredicate = criteriaBuilder.and(criteriaBuilder.greaterThan(fromEmployee.get("eid"), 0),
    			criteriaBuilder.notEqual(fromEmployee.get("firstName"), "David1"));
    select.where(jpaPredicate);

    For more help visit Wikibooks JPA Criteria API documentation

  15. Stored Procedures

    Stored procedures are like named functions that are stored in the database and used to execute native SQL statements to increase the reusability and take advantage of database-specific syntaxes. Stored procedures can accept input parameters and return output after executing the queries.

    Hibernate provides support for executing the stored procedures and capturing their outputs using ProcedureCall & StoredProcedureQuery APIs. We can programmatically configure the procedure names and parameters or we can use the @NamedStoredProcedureQuery annotation to provide stored procedure details and later refer it to other places in the application.

    Note that, under the hood, Hibernate executes the JDBC CallableStatement for fetching the procedure outputs. By default, the CallableStatement is closed upon ending the currently running database transaction, either via calling commit or rollback.

    ProcedureCall getEmployeeById(?,?,?,?): It accepts IN parameter eid and returns the employee details using OUT parameters.

    DELIMITER $$
    CREATE PROCEDURE getEmployeeById (IN eid int, OUT firstName varchar(100), OUT lastName varchar(100), OUT salary float)
    BEGIN
          SELECT e.firstName, e.lastName, e.salary
          INTO firstName, lastName, salary
          from employee e
          where e.eid=eid;
    END$$
    public class Employee implements Serializable {
      @Id
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      private int eid;
      private String firstName;
      private String lastName;
      private double salary;
    }
    Session session = sessionFactory.openSession();
    Transaction transaction = session.beginTransaction();
    for (int i = 1; i <= 2; i++) {
      Employee employee = new Employee();
      employee.setFirstName("David" + i);
      employee.setLastName("Warner" + i);
      employee.setSalary(1000 * i);
      session.persist(employee);
    }
    transaction.commit();
    session.close();
    
    Session session2 = sessionFactory.openSession();
    ProcedureCall procedureCall = session2.createStoredProcedureCall("getEmployeeById");
    procedureCall.registerParameter(1, Integer.class, ParameterMode.IN);
    procedureCall.setParameter(1, 2); // 1st 1 is positional parameter & 2nd 1 is the employee id
    procedureCall.registerParameter(2, String.class, ParameterMode.OUT);
    procedureCall.registerParameter(3, String.class, ParameterMode.OUT);
    procedureCall.registerParameter(4, Double.class, ParameterMode.OUT);
    ProcedureOutputs procedureOutputs = procedureCall.getOutputs();
    Object firstName = procedureOutputs.getOutputParameterValue(2);
    Object lastName = procedureOutputs.getOutputParameterValue(3);
    Object salary = procedureOutputs.getOutputParameterValue(4);
    System.out.println(firstName + " " + lastName + " " + salary);

    We can use either positional parameter or named parameter but we cannot mix both otherwise we shall encounter en exception:

    Exception in thread "main" java.lang.IllegalArgumentException: Cannot mix named parameter with positional parameter registrations

    We must write either complete positional or complete named parameters.

    procedureCall.registerParameter("eid", Integer.class, ParameterMode.IN);
    procedureCall.setParameter("eid", 2);
    procedureCall.registerParameter("firstName", String.class, ParameterMode.OUT);
    procedureCall.registerParameter("lastName", String.class, ParameterMode.OUT);
    procedureCall.registerParameter("salary", Double.class, ParameterMode.OUT);
    ProcedureOutputs procedureOutputs = procedureCall.getOutputs();
    Object firstName = procedureOutputs.getOutputParameterValue("firstName");
    Object lastName = procedureOutputs.getOutputParameterValue("lastName");
    Object salary = procedureOutputs.getOutputParameterValue("salary");
    System.out.println(firstName + " " + lastName + " " + salary);
    Hibernate: insert into Employee (firstName, lastName, salary) values (?, ?, ?)
    Hibernate: insert into Employee (firstName, lastName, salary) values (?, ?, ?)
    
    Hibernate: {call getEmployeeById(?,?,?,?)}
    
    David2 Warner2 2000.0

    StoredProcedureQuery getEmployeeDetailsBySalary(?): We cannot use OUT parameters if we have to fetch a lot of information after the execution of stored procedures. It will create problems in code maintenance. So we can only map the IN parameters because they are generally limited to 1 or 2 values. And we can get the output information in form of Object[].

    DELIMITER //
    CREATE PROCEDURE getEmployeeDetailsBySalary(IN sal FLOAT)
    BEGIN
      SELECT *
      FROM Employee e
      WHERE e.salary = sal;
    END //

    The SELECT * clause selects all four columns from the table so we shall have an Object[] of size 4. This will vary based on the number of columns and the SELECT clause. Also, the size of the List will depend on the number of rows returned after the execution of the stored procedure.

    Here we need to create StoredProcedureQuery using createStoredProcedureQuery() method. This time we need to execute the procedure with getResultList() method.

    Session session = sessionFactory.openSession();
    Transaction transaction = session.beginTransaction();
    for (int i = 1; i <= 2; i++) {
      Employee employee = new Employee();
      employee.setFirstName("David" + i);
      employee.setLastName("Warner" + i);
      employee.setSalary(1200);
      session.persist(employee);
    }
    transaction.commit();
    session.close();
    
    Session session2 = sessionFactory.openSession();
    ProcedureCall procedureCall = session2.createStoredProcedureQuery("getEmployeeDetailsBySalary");
    procedureCall.registerParameter("salary", Double.class, ParameterMode.IN);
    procedureCall.setParameter("salary", 1200);
    List<Object[]> procedureOutputs = procedureCall.getResultList();
    for (Object[] columns : procedureOutputs) {
      System.out.println(columns[0] + " " + columns[1] + " " + columns[2] + " " + columns[3]);
    }
    session2.close();
    Hibernate: create table Employee (eid integer not null auto_increment, firstName varchar(255), lastName varchar(255), salary float(53) not null, primary key (eid)) engine=MyISAM
    
    Hibernate: insert into Employee (firstName, lastName, salary) values (?, ?, ?)
    Hibernate: insert into Employee (firstName, lastName, salary) values (?, ?, ?)
    
    Hibernate: {call getEmployeeDetailsBySalary(?)}
    
    1 David1 Warner1 1200.0
    2 David2 Warner2 1200.0

    @NamedStoredProcedureQuery: This annotation is used to specify a stored procedure query that can be retrieved later by its name. This annotation can be applied to an Entity or mapped superclass. It is important to note that all parameters must be specified in the order in which they occur in the parameter list of the stored procedure in the database. As a great benefit, we can directly map a class to the procedure results.

    public class Constant {
      public static final String NAMED_STORED_PROCEDURE_QUERY_NAME = "getEmployeeDetailsBySalaryProcedure";
      public static final String DB_PROCEDURE_NAME = "getEmployeeDetailsBySalary";
      public static final String COLUMN_NAME = "salary";
    }
    
    @Entity
    @NamedStoredProcedureQuery(name = Constant.NAMED_STORED_PROCEDURE_QUERY_NAME, procedureName = Constant.DB_PROCEDURE_NAME, resultClasses = {
        Employee.class }, parameters = {
            @StoredProcedureParameter(name = Constant.COLUMN_NAME, type = Integer.class, mode = ParameterMode.IN) })
    public class Employee implements Serializable {
      @Id
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      private int eid;
      private String firstName;
      private String lastName;
      private double salary;
    }
    Session session = sessionFactory.openSession();
    Transaction transaction = session.beginTransaction();
    for (int i = 1; i <= 2; i++) {
      Employee employee = new Employee();
      employee.setFirstName("David" + i);
      employee.setLastName("Warner" + i);
      employee.setSalary(1200);
      session.persist(employee);
    }
    transaction.commit();
    session.close();
    
    Session session2 = sessionFactory.openSession();
    ProcedureCall procedureCall = session2.createNamedStoredProcedureQuery(Constant.NAMED_STORED_PROCEDURE_QUERY_NAME);
    List list = procedureCall.setParameter(Constant.COLUMN_NAME, 1200).getResultList();
    for (Object object : list) {
      System.out.println((Employee) object);
    }
    session2.close();
    Hibernate: insert into Employee (firstName, lastName, salary) values (?, ?, ?)
    Hibernate: insert into Employee (firstName, lastName, salary) values (?, ?, ?)
    
    Hibernate: {call getEmployeeDetailsBySalary(?)}
    
    Employee(eid=1, firstName=David1, lastName=Warner1, salary=1200.0)
    Employee(eid=2, firstName=David2, lastName=Warner2, salary=1200.0)
  16. First Level Cache

    Caching is a facility provided by ORM frameworks that helps the users to get fast-running web applications while helping the framework itself to reduce the number of queries made to the database in a single transaction. Hibernate can achieve this by using First Level Cache.

    • First level cache in hibernate is enabled by default and we do not need to do anything to get this functionality working. In fact, we can not disable it even forcefully.
    • First-level cache associated with the Session object and it is available only till the session object is live.
    • The first-level cache is associated with a specific "session" object and other session objects in the application can not see it.
    • The scope of cache objects is of the session. Once the session is closed, cached objects are gone forever.
    • When we query an entity the first time, it is retrieved from the database and stored in the first-level cache associated with hibernate session.
    • If we query the same entity again with the same session object, it will be loaded from the cache and no SQL query will be executed.
    • The loaded entity can be removed from the session using evict() method. The next loading of this entity will again make a database call if it has been removed using evict() method.
    • The whole session cache can be removed using clear() method. It will remove all the entities stored in the cache.
    Session session = sessionFactory.openSession();
    Transaction transaction = session.beginTransaction();
    Employee employee = new Employee();
    employee.setFirstName("David");
    employee.setLastName("Warner");
    employee.setSalary(1200);
    session.persist(employee);
    transaction.commit();
    session.close();
    
    Session session2 = sessionFactory.openSession();
    Employee employee2 = session2.get(Employee.class, 1);
    System.out.println(employee2);
    Employee employee3 = session2.getReference(Employee.class, 1); // equivalent to load() method
    System.out.println(employee3);
    session2.close();
    Hibernate: create table Employee (eid integer not null auto_increment, firstName varchar(255), lastName varchar(255), salary float(53) not null, primary key (eid)) engine=MyISAM
    
    Hibernate: insert into Employee (firstName, lastName, salary) values (?, ?, ?)
    
    Hibernate: select e1_0.eid,e1_0.firstName,e1_0.lastName,e1_0.salary from Employee e1_0 where e1_0.eid=?
    
    Employee(eid=1, firstName=David, lastName=Warner, salary=1200.0)
    Employee(eid=1, firstName=David, lastName=Warner, salary=1200.0)

    Here we can clearly see that only one time of SELECT query has executed and we have fetched Employee two times. 1st time no employee is in the cache. Hence it goes to the database and store into the cache. 2nd time first it checks in the cache whether it is available or not and obviously it is available. So no any SELECT query.

    If we fetch employee with different sessions then definately it will fetch from the database.

    Session session = sessionFactory.openSession();
    Transaction transaction = session.beginTransaction();
    Employee employee = new Employee();
    employee.setFirstName("David");
    employee.setLastName("Warner");
    employee.setSalary(1200);
    session.persist(employee);
    transaction.commit();
    session.close();
    
    Session session2 = sessionFactory.openSession();
    Employee employee2 = session2.get(Employee.class, 1);
    System.out.println(employee2);
    session2.close();
    
    Session session3 = sessionFactory.openSession();
    Employee employee3 = session3.getReference(Employee.class, 1); // equivalent to load() method bcz load() is deprecated
    System.out.println(employee3);
    session3.close();
    Hibernate: create table Employee (eid integer not null auto_increment, firstName varchar(255), lastName varchar(255), salary float(53) not null, primary key (eid)) engine=MyISAM
    Hibernate: insert into Employee (firstName, lastName, salary) values (?, ?, ?)
    
    Hibernate: select e1_0.eid,e1_0.firstName,e1_0.lastName,e1_0.salary from Employee e1_0 where e1_0.eid=?
    Employee(eid=1, firstName=David, lastName=Warner, salary=1200.0)
    
    Hibernate: select e1_0.eid,e1_0.firstName,e1_0.lastName,e1_0.salary from Employee e1_0 where e1_0.eid=?
    Employee(eid=1, firstName=David, lastName=Warner, salary=1200.0)

    Remove cached entity: Though we can not disable the first-level cache in hibernate, we can certainly remove some objects from it when needed. This is done using two methods:

    • evict(): removes a particular object from cache associated with the session
    • clear(): remove all cached objects associated with the session
    Session session = sessionFactory.openSession();
    Transaction transaction = session.beginTransaction();
    Employee employee = new Employee();
    employee.setFirstName("David");
    employee.setLastName("Warner");
    employee.setSalary(1200);
    session.persist(employee);
    transaction.commit();
    session.close();
    
    Session session2 = sessionFactory.openSession();
    Employee employee2 = session2.get(Employee.class, 1);
    System.out.println(employee2);
    session2.evict(employee2);
    // session2.clear();
    
    Employee employee3 = session2.get(Employee.class, 1);
    System.out.println(employee3);
    session2.close();
    Hibernate: create table Employee (eid integer not null auto_increment, firstName varchar(255), lastName varchar(255), salary float(53) not null, primary key (eid)) engine=MyISAM
    
    Hibernate: insert into Employee (firstName, lastName, salary) values (?, ?, ?)
    
    Hibernate: select e1_0.eid,e1_0.firstName,e1_0.lastName,e1_0.salary from Employee e1_0 where e1_0.eid=?
    Employee(eid=1, firstName=David, lastName=Warner, salary=1200.0)
    
    Hibernate: select e1_0.eid,e1_0.firstName,e1_0.lastName,e1_0.salary from Employee e1_0 where e1_0.eid=?
    Employee(eid=1, firstName=David, lastName=Warner, salary=1200.0)
  17. Second Level Cache

    This is separate from the first-level cache and is available to be used globally in SessionFactory scope.

    • The entities stored in the second level cache will be available to all the sessions created using that particular session factory.
    • Once the SessionFactory is closed, all cache associated with it die and the cache manager also closes down.
    • If we have two instances of SessionFactory (highly discouraged), we will have two cache managers in our application and while accessing cache stored in a physical store, we might get unpredictable results like cache-miss.

    How does Second Level Cache Work in Hibernate?

    • Whenever hibernate session tries to load an entity, the very first place it looks for a cached copy of the entity in first-level cache (associated with a particular hibernate Session).
    • If a cached copy of the entity is present in first-level cache, it is returned as the result of load()/get() method.
    • If there is no cached entity in the first-level cache, then the second-level cache is looked up for the cached entity.
    • If second-level cache has the cached entity, it is returned as the result of load() method. But, before returning the entity, it is stored in first level cache also so that the next invocation to load()/get() method for that entity will return the entity from the first level cache itself, and there will not be need to go to the second level cache again.
    • If the entity is not found in first level cache and second level cache also, then a database query is executed and the entity is stored in both cache levels, before returning as the response to load() method.
    • Second-level cache validates itself for modified entities if the modification has been done through hibernate session APIs.
    • If some user or process makes changes directly in the database, there is no way that the second-level cache update itself until “timeToLiveSeconds” duration has passed for that cache region. In this case, it is a good idea to invalidate the whole cache and let hibernate build its cache once again. We can use sessionFactory.evictEntity() in a loop to invalidate the whole Hibernate second-level cache.
    /**
    * Evicts all second level cache hibernate entites. This is generally only
    * needed when an external application modifies the databaase.
    */
    public void evict2ndLevelCache() {
        try {
            Map<String, ClassMetadata> classesMetadata = sessionFactory.getAllClassMetadata();
            for (String entityName : classesMetadata.keySet()) {
                log.info("Evicting Entity from 2nd level cache: " + entityName);
                sessionFactory.evictEntity(entityName);
            }
        } catch (Exception e) {
            log.error(Level.SEVERE, "SessionController",
            "evict2ndLevelCache",
            "Error evicting 2nd level hibernate cache entities: ", e);
        }
    }

    Ehcache: Ehcache is a very popular open-source project available under the Apache 2.0 license. It’s a multi-purpose, highly scalable cache implementation for Java applications.

    Configuring Ehcache 3 with Hibernate 6: The required configuration and dependencies to integrate Ehcache with Hibernate depend on the Ehcache version we want to use in our project. Hibernate provides a proprietary integration for the older Ehcache 2.x releases. The newer Ehcache 3.x releases implement the JCache specification. Hibernate provides a generic integration for all caches that implement that specification. Since version 3, Ehcache has been implementing the JCache specification. We can integrate it using Hibernate’s hibernate-jcache module. It provides a generic integration for all JCache compatible cache implementations.

    <dependency>
      <groupId>org.hibernate.orm</groupId>
      <artifactId>hibernate-jcache</artifactId>
      <version>6.1.4.Final</version>
    </dependency>

    In addition to the dependency, we also need to enable the 2nd level cache and we can do that by adding a property hibernate.cache.use_second_level_cache in our hibernate.cfg.xml file and set it to true. Now we need to add the hibernate.cache.region.factory_class property to the hibernate.cfg.xml file and set it to jcache.

    <property name="hibernate.cache.use_second_level_cache">true</property>
    <property name="hibernate.cache.region.factory_class">jcache</property>

    Hibernate then uses the default JCache provider to create the default CacheManager. It also uses a default configuration to create the caches. If we want to provide a configuration, which we should, we need to specify the CacheManager and a path to the cache configuration in our hibernate.cfg.xml file.

    <property name="hibernate.javax.cache.provider">org.ehcache.jsr107.EhcacheCachingProvider</property>
    <property name="hibernate.javax.cache.uri">ehcache.xml</property>
    <!-- src/main/resources/ehcache.xml -->
    <?xml version="1.0" encoding="utf-8"?>
    <config xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xmlns='http://www.ehcache.org/v3'
      xsi:schemaLocation="
            http://www.ehcache.org/v3 http://www.ehcache.org/schema/ehcache-core-3.0.xsd">
    
      <cache alias="ready-cache">
        <key-type>java.lang.Integer</key-type>
        <value-type>com.altafjava.cache.secondlevel.Employee</value-type>
        <heap unit="entries">100</heap>
      </cache>
    </config>
    import org.hibernate.annotations.Cache;
    import org.hibernate.annotations.CacheConcurrencyStrategy;
    import jakarta.persistence.Cacheable;
    @Entity
    @Cacheable
    @Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
    public class Employee implements Serializable {
      @Id
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      private int eid;
      private String firstName;
      private String lastName;
      private double salary;
    }
    // Save Employee
    SessionFactory sessionFactory = HibernateUtil.getSessionFactory();
    Session session = sessionFactory.openSession();
    Transaction transaction = session.beginTransaction();
    Employee employee = new Employee();
    employee.setFirstName("David");
    employee.setLastName("Warner");
    employee.setSalary(1200);
    session.persist(employee);
    transaction.commit();
    session.close();
    // Fetch Employee
    int employeeId = 1;
    Employee employee = session.getReference(Employee.class, employeeId); // from DB
    System.out.println(employee);
    employee = session.getReference(Employee.class, employeeId); // from 1L cache
    System.out.println(employee);
    session.evict(employee);
    session.close();
    System.out.println("EntityFetchCount: " + statistics.getEntityFetchCount());
    System.out.println("2L cache hit count: " + statistics.getSecondLevelCacheHitCount());
    
    Session session2 = sessionFactory.openSession();
    employee = session2.getReference(Employee.class, employeeId);// from 2L cache
    System.out.println(employee);
    session2.evict(employee);
    employee = session2.getReference(Employee.class, employeeId); // from 2L cache
    System.out.println(employee);
    session2.close();
    System.out.println("EntityFetchCount: " + statistics.getEntityFetchCount());
    System.out.println("2L cache hit count: " + statistics.getSecondLevelCacheHitCount());
    
    Session session3 = sessionFactory.openSession();
    employee = session3.getReference(Employee.class, employeeId);// from 2L cache
    System.out.println(employee);
    session3.clear();
    employee = session3.getReference(Employee.class, employeeId); // from 2L cache
    System.out.println(employee);
    session3.close();
    System.out.println("EntityFetchCount: " + statistics.getEntityFetchCount());
    System.out.println("2L cache hit count: " + statistics.getSecondLevelCacheHitCount());
    Hibernate: create table Employee (eid integer not null auto_increment, firstName varchar(255), lastName varchar(255), salary float(53) not null, primary key (eid)) engine=MyISAM
    
    Hibernate: insert into Employee (firstName, lastName, salary) values (?, ?, ?)
    
    Hibernate: select e1_0.eid,e1_0.firstName,e1_0.lastName,e1_0.salary from Employee e1_0 where e1_0.eid=?
    
    Employee(eid=1, firstName=David, lastName=Warner, salary=1200.0)
    Employee(eid=1, firstName=David, lastName=Warner, salary=1200.0)
    EntityFetchCount: 1
    2L cache hit count: 0
    
    Employee(eid=1, firstName=David, lastName=Warner, salary=1200.0)
    Employee(eid=1, firstName=David, lastName=Warner, salary=1200.0)
    EntityFetchCount: 1
    2L cache hit count: 2
    
    Employee(eid=1, firstName=David, lastName=Warner, salary=1200.0)
    Employee(eid=1, firstName=David, lastName=Warner, salary=1200.0)
    EntityFetchCount: 1
    2L cache hit count: 4

    The @Cacheable annotation is used to specify whether an entity should be stored in the second-level cache. And the @Cache annotation is used to specify the CacheConcurrencyStrategy(READ_ONLY, READ_WRITE, NONSTRICT_READ_WRITE, TRANSACTIONAL) of a root entity or a collection.

    QueryCache: Hibernate also supports the QueryCache, which can store the result of a query. We need to activate it in the hibernate.cfg.xml file by setting the parameter hibernate.cache.use_query_cache to true and defining a hibernate.cache.region.factory_class. This is only useful for queries that are run frequently with the same parameters.

    Note that the query cache does not cache the state of the actual entities in the result set. It caches only identifier values and results of the value type. So the query cache should always be used in conjunction with the second-level cache.

    <property name="hibernate.cache.use_query_cache">true</property>
    <property name="hibernate.cache.region.factory_class">jcache</property>
    SessionFactory sessionFactory = HibernateUtil.getSessionFactory();
    Session session = sessionFactory.openSession();
    
    Query<Employee> query = session.createQuery("from Employee where eid=:id");
    query.setParameter("id", 1);
    query.setCacheable(true);
    Employee employee = query.uniqueResult();
    System.out.println(employee);
    
    query = session.createQuery("from Employee where eid=:id");
    query.setParameter("id", 1);
    query.setCacheable(true);
    employee = query.uniqueResult();
    System.out.println(employee);
    
    query = session.createQuery("from Employee where eid=:id");
    query.setCacheable(true);
    query.setParameter("id", 1);
    employee = query.uniqueResult();
    System.out.println(employee);

    We have executed query 3 times but it fetch the data from database only first time. For the remaining queries it fetch from the cache.

    Hibernate: select e1_0.eid,e1_0.firstName,e1_0.lastName,e1_0.salary from Employee e1_0 where e1_0.eid=? and e1_0.firstName=?
    
    Employee(eid=1, firstName=David, lastName=Warner, salary=1200.0)
    Employee(eid=1, firstName=David, lastName=Warner, salary=1200.0)
    Employee(eid=1, firstName=David, lastName=Warner, salary=1200.0)

    The combination of the query & the parameter values that we have passed used as a key and the result value become the value.

    Key: [select e1_0.eid,e1_0.firstName,e1_0.lastName,e1_0.salary from Employee e1_0 where e1_0.eid=? and e1_0.firstName=?, [1, "David"]]

    Value: [Employee(eid=1, firstName=David, lastName=Warner, salary=1200.0)]

  18. Connection Pooling(C3P0)

    By default, Hibernate uses JDBC connections in order to interact with a database. Creating the database connections is expensive, probably the most expensive operation. For this reason, we are advised to use a connection pool that can store the opened connections ahead of time and close them only when they are not needed.

    Hibernate is designed to use a connection pool by default, an internal implementation. However, Hibernate’s built-in connection pooling isn't designed for production use. In production, we must use an external connection pool by using either a database connection provided by JNDI or an external connection pool configured via parameters and classpath. C3P0 is an example of an external connection pool.

    To configure c3p0 with hibernate we just need to add Hibernates c3p0 connection provider hibernate-c3p0 as dependency in the pom.xml. The best part is that the whole configuration of C3P0 with hibernate is really very easy. In most cases, if we do not have any other connection provider, just adding any hibernate-c3p0 dependency in the pom.xml will configure it with defaults. But if we want to configure with our own values then we need to add those configuration in the hibernate.cfg.xml file.

    If there is more than one connection pool then to enforce c3p0 poling, we can provide the provider_class property.

    <property name="hibernate.connection.provider_class">
      org.hibernate.connection.C3P0ConnectionProvider
    </property>

    Detailed C3P0 configuration:

    <property name="hibernate.c3p0.min_size">10</property>
    <property name="hibernate.c3p0.max_size">20</property>
    <property name="hibernate.c3p0.acquire_increment">1</property>
    <property name="hibernate.c3p0.idle_test_period">3000</property>
    <property name="hibernate.c3p0.max_statements">50</property>
    <property name="hibernate.c3p0.timeout">1800</property>
    <property name="hibernate.c3p0.validate">1800</property>

    We can find detailed information about the above configuration switches in official documentation.

    Debugging Connection Leaks: Sometimes applications talk to many other applications and some applications or interactions take a longer time to respond. This can overwhelm the connection pool (when the pool grows to maxPoolSize) and degrade the performance of the whole application. c3p0 can help us debug the pool where connections have been checked out and don’t get checked in.

    <property name="hibernate.c3p0.unreturnedConnectionTimeout">30</property>
    <property name="hibernate.c3p0.debugUnreturnedConnectionStackTraces">true</property>
    • unreturnedConnectionTimeout helps in fixing leaks. It defines the time (in seconds) to how long a Connection may remain checked out. Checked-out Connections that exceed this limit will be destroyed, and then created a new one in the pool.

    • debugUnreturnedConnectionStackTraces helps in debugging the root cause. when set to true, whenever an unreturned Connection times out, that stack trace will be printed, revealing where a Connection was checked out that was not checked in.

    Application Logs without C3P0:

    WARN: HHH000022: c3p0 properties were encountered, but the c3p0 provider class was not found on the classpath; these properties are going to be ignored.
    Nov 02, 2022 7:27:24 AM org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl configure
    WARN: HHH10001002: Using built-in connection pool (not intended for production use)
    Nov 02, 2022 7:27:24 AM org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl buildCreator
    INFO: HHH10001005: Loaded JDBC driver class: com.mysql.jdbc.Driver
    Nov 02, 2022 7:27:24 AM org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl buildCreator
    INFO: HHH10001012: Connecting with JDBC URL [jdbc:mysql://localhost:3306/test?createDatabaseIfNotExist=true]
    Nov 02, 2022 7:27:24 AM org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl buildCreator
    INFO: HHH10001001: Connection properties: {password=****, user=root}
    Nov 02, 2022 7:27:24 AM org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl buildCreator
    INFO: HHH10001003: Autocommit mode: false
    Nov 02, 2022 7:27:24 AM org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl$PooledConnections <init>
    INFO: HHH10001115: Connection pool size: 20 (min=1)
    

    Application Logs with C3P0:

    Nov 02, 2022 7:24:45 AM com.mchange.v2.c3p0.C3P0Registry
    INFO: Initializing c3p0-0.9.5.5 [built 11-December-2019 22:18:33 -0800; debug? true; trace: 10]
    Nov 02, 2022 7:24:45 AM org.hibernate.c3p0.internal.C3P0ConnectionProvider configure
    INFO: HHH10001007: JDBC isolation level: <unknown>
    Nov 02, 2022 7:24:45 AM com.mchange.v2.c3p0.impl.AbstractPoolBackedDataSource
    INFO: Initializing c3p0 pool... com.mchange.v2.c3p0.PoolBackedDataSource@1f406d1d [ connectionPoolDataSource -> com.mchange.v2.c3p0.WrapperConnectionPoolDataSource@9e568004 [ acquireIncrement -> 1, acquireRetryAttempts -> 30, acquireRetryDelay -> 1000, autoCommitOnClose -> false, automaticTestTable -> null, breakAfterAcquireFailure -> false, checkoutTimeout -> 0, connectionCustomizerClassName -> null, connectionTesterClassName -> com.mchange.v2.c3p0.impl.DefaultConnectionTester, contextClassLoaderSource -> caller, debugUnreturnedConnectionStackTraces -> true, factoryClassLocation -> null, forceIgnoreUnresolvedTransactions -> false, forceSynchronousCheckins -> false, identityToken -> 1hgeu41asezvbiq15i1mzd|16943e88, idleConnectionTestPeriod -> 3000, initialPoolSize -> 10, maxAdministrativeTaskTime -> 0, maxConnectionAge -> 0, maxIdleTime -> 1800, maxIdleTimeExcessConnections -> 0, maxPoolSize -> 20, maxStatements -> 50, maxStatementsPerConnection -> 0, minPoolSize -> 10, nestedDataSource -> com.mchange.v2.c3p0.DriverManagerDataSource@acbcee36 [ description -> null, driverClass -> null, factoryClassLocation -> null, forceUseNamedDriverClass -> false, identityToken -> 1hgeu41asezvbiq15i1mzd|5305c37d, jdbcUrl -> jdbc:mysql://localhost:3306/test?createDatabaseIfNotExist=true, properties -> {password=******, user=******} ], preferredTestQuery -> null, privilegeSpawnedThreads -> false, propertyCycle -> 0, statementCacheNumDeferredCloseThreads -> 0, testConnectionOnCheckin -> false, testConnectionOnCheckout -> false, unreturnedConnectionTimeout -> 30, usesTraditionalReflectiveProxies -> false; userOverrides: {} ], dataSourceName -> null, extensions -> {}, factoryClassLocation -> null, identityToken -> 1hgeu41asezvbiq15i1mzd|3e521715, numHelperThreads -> 3 ]
    
  19. Pagination

    To paginate the query results using HQL APIs & ScrollableResults interface in Hibernate. Pagination helps in cases when the number of rows in query output is very high and fetching all records will badly affect the performance of the application.

    The HQL methods Query#setMaxResults() & Query#setFirstResult() are used to limit the number of results and control pagination. It is the most common technique to implement pagination in Hibernate. Lets save the 10 records of Employee and fetch few of the records by using the above methods.

    for (int i = 1; i <= 10; i++) {
      Employee employee = new Employee();
      employee.setFirstName("David" + i);
      employee.setLastName("Warner" + i);
      employee.setSalary(1200 * i);
      session.persist(employee);
    }
    Query<Employee> query = session.createQuery("from Employee");
    query.setFirstResult(2);// start index/(offset+1)
    query.setMaxResults(5);// no of rows/records
    List<Employee> employees = query.list();
    for (Employee employee : employees) {
      System.out.println(employee);
    }
    Hibernate: select e1_0.eid,e1_0.firstName,e1_0.lastName,e1_0.salary from Employee e1_0 limit ?,?
    
    Employee(eid=3, firstName=David3, lastName=Warner3, salary=3600.0)
    Employee(eid=4, firstName=David4, lastName=Warner4, salary=4800.0)
    Employee(eid=5, firstName=David5, lastName=Warner5, salary=6000.0)
    Employee(eid=6, firstName=David6, lastName=Warner6, salary=7200.0)
    Employee(eid=7, firstName=David7, lastName=Warner7, salary=8400.0)

    As we can see in the generated sql, Hibernate uses the OFFSET &LIMIT clause.

    MySQL limit syntax: offset is optional

    SELECT
      select_list
    FROM
        table_name
    LIMIT [offset,] row_count;
    • OFFSET: It specifies how many records shall be skipped before the first record gets returned
    • LIMIT: It defines the maximum number of records returned by the query.

    Lets say we use limit 2, 5. Here 2 is the offset & 5 is the limit. It means it will skip the 1st & 2nd row and fetch from the 3nd row till 7th row(2+5=7). Means max it will fetch 5 rows/records.

    Note: Stored procedure queries cannot be paged with setFirstResult()/setMaxResults().

    Example: To demonstrate the pagination functionality, we have created a class to represent the paged result that can be returned to the client.

    public class PaginationResult<E> {
      private int currentPageNumber;
      private int lastPageNumber;
      private int pageSize;
      private long totalRecords;
      private List<E> records;
    }

    Typically, in pagination, the application gets the requested pageNumber along with the numberOfRecords/pageSize. These two act as the request parameters. We have written a function that accepts the above two arguments and returns an instance of PaginationResult that has all the information needed to build a pagination UI on the client side.

    public class PaginationService {
      public PaginationResult<Employee> paginateUsingHql(int pageNumber, int pageSize) {
        Long totalRecords = 0L;
        List<Employee> employees = Collections.emptyList();
        SessionFactory sessionFactory = HibernateUtil.getSessionFactory();
        try (Session session = sessionFactory.openSession()) {
          TypedQuery countQuery = session.createQuery("select count(emp.eid) from Employee emp");
          totalRecords = (Long) countQuery.getSingleResult();
          System.out.println("totalRecords:" + totalRecords);
        }
    
        int lastPage = 0;
        if (totalRecords % pageSize == 0) {
          lastPage = (int) (totalRecords / pageSize);
        } else {
          lastPage = (int) ((totalRecords / pageSize) + 1);
        }
        int offset = pageSize * (pageNumber - 1);
        System.out.println("currentPage:" + pageNumber + "  lastPage:" + lastPage + "  pageSize:" + pageSize + "  offset:" + offset);
    
        try (Session session = sessionFactory.openSession()) {
          TypedQuery<Employee> fromQuery = session.createQuery("from Employee");
          fromQuery.setFirstResult(offset);
          fromQuery.setMaxResults(pageSize);
          employees = fromQuery.getResultList();
        }
        PaginationResult<Employee> paginationResult = new PaginationResult<>();
        paginationResult.setCurrentPageNumber(pageNumber);
        paginationResult.setLastPageNumber(lastPage);
        paginationResult.setPageSize(pageSize);
        paginationResult.setTotalRecords(totalRecords);
        paginationResult.setRecords(employees);
        return paginationResult;
      }
    }

    To test if the pagination is working as expected, we have inserted total of 24 records in the database. And we can test by calling paginateUsingHql() method of PaginationService.

    // OffsetLimitTest.saveEmployees(24);
    PaginationService paginationService = new PaginationService();
    PaginationResult<Employee> paginationResult = paginationService.paginateUsingHql(2, 7);
    List<Employee> employees = paginationResult.getRecords();
    for (Employee employee : employees) {
      System.out.println(employee);
    }
    Hibernate: select count(e1_0.eid) from Employee e1_0
    totalRecords:24
    currentPage:2  lastPage:4  pageSize:7  offset:7
    
    Hibernate: select e1_0.eid,e1_0.firstName,e1_0.lastName,e1_0.salary from Employee e1_0 limit ?,?
    
    Employee(eid=8, firstName=David8, lastName=Warner8, salary=9600.0)
    Employee(eid=9, firstName=David9, lastName=Warner9, salary=10800.0)
    Employee(eid=10, firstName=David10, lastName=Warner10, salary=12000.0)
    Employee(eid=11, firstName=David11, lastName=Warner11, salary=13200.0)
    Employee(eid=12, firstName=David12, lastName=Warner12, salary=14400.0)
    Employee(eid=13, firstName=David13, lastName=Warner13, salary=15600.0)
    Employee(eid=14, firstName=David14, lastName=Warner14, salary=16800.0)

    Pagination using ScrollableResults: The org.hibernate.Query interface offers Query#scroll() for reading query results incrementally while maintaining an open JDBC ResultSet mapped to a server-side cursor. The scroll() method returns a ScrollableResults which wraps an underlying JDBC scrollable ResultSet. We can use ScrollableResults#scroll() to set the starting position of the cursor and then ScrollableResults#next() to get sequentially iterate over the number of records on the page.

    import org.hibernate.query.Query;
    
    Query<Employee> fromQuery = session.createQuery("from Employee", Employee.class);
    try (ScrollableResults<Employee> scrollableResults = fromQuery.scroll()) {
      while (scrollableResults.next() && scrollableResults.getRowNumber() >= 0 && scrollableResults.getRowNumber() < 10) {
        Employee employee = scrollableResults.get();
        System.out.println(employee);
      }
    }

    This will print the employee information from eid 1 to 10. One good thing about this approach(ScrollableResults) is that we do not need to execute an extra query(select count(e.eid) from Employee e) to get the totalRecords count. Just move the cursor to the last record in the result set, and get the current row number.

    resultScroll.last();
    totalRecords = resultScroll.getRowNumber() + 1;  // JDBC ResultSet Cursor starts with 0

    Example: Similar to the previous pagination example, let’s build a method that uses the ScrollableResults to paginate based on given method arguments pageNumber and pageSize. In the end, it will return an instance of PaginationResult with all the employee information populated into it.

    public PaginationResult<Employee> paginateUsingScrollableResults(int pageNumber, int pageSize) {
      List<Employee> employees = new ArrayList<>();
      int lastPage = 0, totalRecords = 0;
      SessionFactory sessionFactory = HibernateUtil.getSessionFactory();
      try (Session session = sessionFactory.openSession()) {
        Query<Employee> fromQuery = session.createQuery("from Employee", Employee.class);
        ScrollableResults<Employee> scrollableResults = fromQuery.scroll();
        boolean hasRecords = scrollableResults.first();
        if (hasRecords) {
          int startIndex = pageSize * (pageNumber - 1);
          int endPosition = pageSize * pageNumber;
          hasRecords = scrollableResults.scroll(startIndex);
          if (hasRecords) {
            do {
              Employee employee = scrollableResults.get();
              employees.add(employee);
            } while (scrollableResults.next() && scrollableResults.getRowNumber() >= startIndex
                && scrollableResults.getRowNumber() < endPosition);
          }
        }
      }
      PaginationResult<Employee> paginationResult = new PaginationResult<>();
      paginationResult.setCurrentPageNumber(pageNumber);
      paginationResult.setLastPageNumber(lastPage);
      paginationResult.setPageSize(pageSize);
      paginationResult.setTotalRecords(totalRecords);
      paginationResult.setRecords(employees);
      return paginationResult;
    }
    // OffsetLimitTest.saveEmployees(24);
    PaginationService paginationService = new PaginationService();
    PaginationResult<Employee> paginationResult = paginationService.paginateUsingScrollableResults(2, 7);
    List<Employee> employees = paginationResult.getRecords();
    for (Employee employee : employees) {
      System.out.println(employee);
    }
    Hibernate: select e1_0.eid,e1_0.firstName,e1_0.lastName,e1_0.salary from Employee e1_0
    
    Employee(eid=8, firstName=David8, lastName=Warner8, salary=9600.0)
    Employee(eid=9, firstName=David9, lastName=Warner9, salary=10800.0)
    Employee(eid=10, firstName=David10, lastName=Warner10, salary=12000.0)
    Employee(eid=11, firstName=David11, lastName=Warner11, salary=13200.0)
    Employee(eid=12, firstName=David12, lastName=Warner12, salary=14400.0)
    Employee(eid=13, firstName=David13, lastName=Warner13, salary=15600.0)
    Employee(eid=14, firstName=David14, lastName=Warner14, salary=16800.0)
  20. Hibernate Interceptors

    Interceptors, as the name suggests, provide callbacks to certain events that occur inside Hibernate. It helps in implementing AOP style cross-cutting concerns and the extension of Hibernate functionality. To create a new Interceptor in Hibernate, we need to implement the org.hibernate.Interceptor interface. This interface provides methods to inspect and/or manipulate properties of a persistent object before it is saved, updated, deleted or loaded.

    Before Hibernate 6.0, extending the EmptyInterceptor was a preferred way to override only the necessary methods because to implement Interceptor, we have to implement all 14 methods in the interface. This was obviously not suitable until we had a very strong reason to do it. Since Hibernate 6.0, EmptyInterceptor has been deprecated. And the methods inside Interceptor interface have been made default methods, so we only need to override only the necessary method now. Interceptor interface provides the following important methods for intercepting specific events:

    • afterTransactionBegin(): Called when a Hibernate transaction is begun.
    • afterTransactionCompletion(): Called after a transaction is committed or rolled back.
    • beforeTransactionCompletion(): Called before a transaction is committed (but not before rollback).
    • onCollectionRecreate(): Called before a collection is (re)created.
    • onCollectionRemove(): Called before a collection is deleted.
    • onCollectionUpdate(): Called before a collection is updated.
    • onDelete(): Called before an object is deleted.
    • onFlushDirty(): Called when an object is detected to be dirty, during a flush.
    • onLoad(): Called just before an object is initialized.
    • onSave(): Called before an object is saved.
    • postFlush(): Called after a flush.
    • preFlush(): Called before a flush.

    A very good article has been written in this blog regarding realtime usecase of Hibernate Interceptor. This was written in 2019. It means some of the classes & methods might have been deprecated but the concept is very useful. Please visit at least once for clear understanding. How to change the table name of an Hibernate entity at run-time?

  21. Batch Processing

    Hibernate leverages the JDBC's batching capability that batches together multiple SQL statements as a single PreparedStatement. By default, batch processing is disabled in Hibernate. So if we persist 10 new entities then 10 separate SQL INSERT statements will be executed. The same is true for a million records. Any application’s performance will keep degrading in proportion to the number of rows increases and we might face the OutOfMemoryError. To enable the batch processing, we need to set the hibernate.jdbc.batch_size property to a number bigger than 0.

    Insert without batch enabled: Lets say we want to insert 10 records in the database then first, hibernate queues all the statements in the current persistent context. When the transaction was committed, all the statements were executed at the end of the method in separate query with batch-size 0.

    Note: Hibernate disables insert batching at the JDBC level transparently if we use an IDENTITY identifier generator in the entity class. So we can either use sequence, table generator or manually we can put the ids.

    @Entity
    public class Employee implements Serializable {
      @Id
      private int eid;
      private String firstName;
      private String lastName;
      private double salary;
    }
    Transaction transaction = session.beginTransaction();
    for (int i = 1; i <= 10; i++) {
      System.out.println("Statement Queued : " + i);
      Employee employee = new Employee();
      employee.setEid(i);
      employee.setFirstName("David" + i);
      employee.setLastName("Warner" + i);
      employee.setSalary(1000 * i);
      session.persist(employee);
    }
    transaction.commit();
    Statement Queued : 1
    Statement Queued : 2
    Statement Queued : 3
    ...
    ...
    Statement Queued : 9
    Statement Queued : 10
    
    Hibernate: insert into Employee (firstName, lastName, salary, eid) values (?, ?, ?, ?)
    Name:, Connection:3, Time:2, Success:True
    Type:Prepared, Batch:False, QuerySize:1, BatchSize:0
    Query:["insert into Employee (firstName, lastName, salary, eid) values (?, ?, ?, ?)"]
    Params:[(David1,Warner1,1000.0,1)]
    
    Hibernate: insert into Employee (firstName, lastName, salary, eid) values (?, ?, ?, ?)
    Name:, Connection:3, Time:1, Success:True
    Type:Prepared, Batch:False, QuerySize:1, BatchSize:0
    Query:["insert into Employee (firstName, lastName, salary, eid) values (?, ?, ?, ?)"]
    Params:[(David2,Warner2,2000.0,2)]
    ...
    ...
    Hibernate: insert into Employee (firstName, lastName, salary, eid) values (?, ?, ?, ?)
    Name:, Connection:3, Time:0, Success:True
    Type:Prepared, Batch:False, QuerySize:1, BatchSize:0
    Query:["insert into Employee (firstName, lastName, salary, eid) values (?, ?, ?, ?)"]
    Params:[(David10,Warner10,10000.0,10)]
    

    Here, we can clearly see that Batch is false, QuerySize is 1 & BatchSize is 0. How did we printed this information?

    Log Batch Statements: As we know, hibernate relies on the low-level JDBC APIs to create batches of queued statements so to enable logging these statements we must intercept the calls at the datasource level. The datasource-proxy is one such library that helps in creating a proxy around the original datasource used by the application.Never use this dependency in Production. This might be vulnerable for your database. Just to check whether batch is enabled or not, we are using in local.

    <dependency>
      <groupId>net.ttddyy</groupId>
      <artifactId>datasource-proxy</artifactId>
      <version>1.8</version>
    </dependency>

    Now create the ProxyDataSource manually and inject into StandardServiceRegistry in HibernateUtil.

    MysqlDataSource mysqlDataSource = new MysqlDataSource();
    mysqlDataSource.setUrl("jdbc:mysql://localhost:3306/test?createDatabaseIfNotExist=true");
    mysqlDataSource.setUser("root");
    mysqlDataSource.setPassword("root");
    
    ProxyDataSource proxyDataSource = ProxyDataSourceBuilder.create(mysqlDataSource).asJson().countQuery().logQueryToSysOut()
        .multiline().build();
    
    Map<String, Object> dataSourceMap = new LinkedHashMap<>();
    dataSourceMap.put(Environment.DATASOURCE, getDataSource());
    StandardServiceRegistry standardServiceRegistry = new StandardServiceRegistryBuilder().configure().applySettings(dataSourceMap).build();
    Metadata metadata = new MetadataSources().getMetadataBuilder(standardServiceRegistry).build();
    SessionFactory sessionFactory = metadata.getSessionFactoryBuilder().build();

    Enable Batch Processing: To enable the batch processing, we need to set the hibernate.jdbc.batch_size property to a number bigger than 0.

    <property name="hibernate.jdbc.batch_size">5</property>

    If we’re using Spring Boot, we can define it as an application property:

    spring.jpa.properties.hibernate.jdbc.batch_size = 5
    

    To configure Session specific batch size, we can use setJdbcBatchSize() method.

    //Using Session
    session.setJdbcBatchSize(50);
    
    //Using EntityManager
    entityManager.unwrap(Session.class).setJdbcBatchSize(50);

    Now we have enabled the batch processing. Lets see the log for the same above program.

    Statement Queued : 1
    Statement Queued : 2
    Statement Queued : 3
    Statement Queued : 4
    Statement Queued : 5
    Statement Queued : 6
    Statement Queued : 7
    Statement Queued : 8
    Statement Queued : 9
    Statement Queued : 10
    Hibernate: insert into Employee (firstName, lastName, salary, eid) values (?, ?, ?, ?)
    Hibernate: insert into Employee (firstName, lastName, salary, eid) values (?, ?, ?, ?)
    Hibernate: insert into Employee (firstName, lastName, salary, eid) values (?, ?, ?, ?)
    Hibernate: insert into Employee (firstName, lastName, salary, eid) values (?, ?, ?, ?)
    Hibernate: insert into Employee (firstName, lastName, salary, eid) values (?, ?, ?, ?)
    
    Name:, Connection:3, Time:3, Success:True
    Type:Prepared, Batch:True, QuerySize:1, BatchSize:5
    Query:["insert into Employee (firstName, lastName, salary, eid) values (?, ?, ?, ?)"]
    Params:[(David1,Warner1,1000.0,1),(David2,Warner2,2000.0,2),(David3,Warner3,3000.0,3),(David4,Warner4,4000.0,4),(David5,Warner5,5000.0,5)]
    Hibernate: insert into Employee (firstName, lastName, salary, eid) values (?, ?, ?, ?)
    Hibernate: insert into Employee (firstName, lastName, salary, eid) values (?, ?, ?, ?)
    Hibernate: insert into Employee (firstName, lastName, salary, eid) values (?, ?, ?, ?)
    Hibernate: insert into Employee (firstName, lastName, salary, eid) values (?, ?, ?, ?)
    Hibernate: insert into Employee (firstName, lastName, salary, eid) values (?, ?, ?, ?)
    
    Name:, Connection:3, Time:1, Success:True
    Type:Prepared, Batch:True, QuerySize:1, BatchSize:5
    Query:["insert into Employee (firstName, lastName, salary, eid) values (?, ?, ?, ?)"]
    Params:[(David6,Warner6,6000.0,6),(David7,Warner7,7000.0,7),(David8,Warner8,8000.0,8),(David9,Warner9,9000.0,9),(David10,Warner10,10000.0,10)]
    

    Here, we can clearly see that Batch is True, QuerySize is 1 but BatchSize is 5. It means, batch processing is enabled and working when we commit the transaction. Hibernate is sending the INSERT statements in batches of 5. At the JDBC level, these batched transactions are grouped into a single INSERT statement. So for every 5 employees, there is only 1 INSERT statement at the database level. But again we have a problem. As we can see, It first queues all the entities into the context and waits for committing the transaction. This can be a serious issue when we have to queue thousands of entities instances into memory (1st-level cache) before flushing them to the database. For sufficient large batches, it can lead to OutOfMemoryError.

    Batch Insert: To overcome this problem, we need to flush and clear the session periodically. The session's flush() method triggers a transaction synchronization that sends all changes in the persistent entities to the database. Flushing is the process of synchronizing the underlying persistent store with a persistable state held in memory. The session’s clear() clears the session. It evicts all loaded instances from Session and cancels all pending saves, updates and deletions. In the given example, we are flushing and clearing the session after each batch (of size 5). So now, we queue 5 employees in the session and use the flush() method to insert these 5 employees in the database in a single batch statement. We can optimize more by disabling the second-level cache completely by settning this property in hibernate.cfg.xml.

    hibernate.cache.use_second_level_cache=false
    Transaction transaction = session.beginTransaction();
    for (int i = 1; i <= 10; i++) {
      System.out.println("Statement Queued : " + i);
      Employee employee = new Employee();
      employee.setFirstName("David" + i);
      employee.setLastName("Warner" + i);
      employee.setSalary(1000 * i);
      session.persist(employee);
      if (i % 5 == 0) { // same as the JDBC batch size
        session.flush(); // flush a batch of inserts and release memory:
        session.clear();
      }
    }
    transaction.commit();
    Statement Queued : 1
    Statement Queued : 2
    Statement Queued : 3
    Statement Queued : 4
    Statement Queued : 5
    Hibernate: insert into Employee (firstName, lastName, salary, eid) values (?, ?, ?, ?)
    Hibernate: insert into Employee (firstName, lastName, salary, eid) values (?, ?, ?, ?)
    Hibernate: insert into Employee (firstName, lastName, salary, eid) values (?, ?, ?, ?)
    Hibernate: insert into Employee (firstName, lastName, salary, eid) values (?, ?, ?, ?)
    Hibernate: insert into Employee (firstName, lastName, salary, eid) values (?, ?, ?, ?)
    
    Name:, Connection:3, Time:3, Success:True
    Type:Prepared, Batch:True, QuerySize:1, BatchSize:5
    Query:["insert into Employee (firstName, lastName, salary, eid) values (?, ?, ?, ?)"]
    Params:[(David1,Warner1,1000.0,1),(David2,Warner2,2000.0,2),(David3,Warner3,3000.0,3),(David4,Warner4,4000.0,4),(David5,Warner5,5000.0,5)]
    Statement Queued : 6
    Statement Queued : 7
    Statement Queued : 8
    Statement Queued : 9
    Statement Queued : 10
    Hibernate: insert into Employee (firstName, lastName, salary, eid) values (?, ?, ?, ?)
    Hibernate: insert into Employee (firstName, lastName, salary, eid) values (?, ?, ?, ?)
    Hibernate: insert into Employee (firstName, lastName, salary, eid) values (?, ?, ?, ?)
    Hibernate: insert into Employee (firstName, lastName, salary, eid) values (?, ?, ?, ?)
    Hibernate: insert into Employee (firstName, lastName, salary, eid) values (?, ?, ?, ?)
    
    Name:, Connection:3, Time:2, Success:True
    Type:Prepared, Batch:True, QuerySize:1, BatchSize:5
    Query:["insert into Employee (firstName, lastName, salary, eid) values (?, ?, ?, ?)"]
    Params:[(David6,Warner6,6000.0,6),(David7,Warner7,7000.0,7),(David8,Warner8,8000.0,8),(David9,Warner9,9000.0,9),(David10,Warner10,10000.0,10)]
    

    We can notice the logs above. A batch of 5 statements in one query. Now this is much better code and provides excellent memory and runtime performance.

    Batch Inserts for Multiple Entities: A limitation of hibernate batching is that it allows only one type of entity in a single batch. For a different entity, a second batch will be created. Let us understand with an example. Let’s create a 10 Employees and some Accounts associated with the Employee. In the following example, we are creating 10 employees and adding 3 accounts to each employee. It makes a total of 10 employees and 30 accounts.

    Transaction transaction = session.beginTransaction();
    for (int i = 1; i <= 10; i++) {
      System.out.print("Employee Statement Queued: " + i + ", ");
      List<Account> accounts = new ArrayList<>();
      Employee employee = new Employee();
      employee.setEid(i);
      employee.setFirstName("David" + i);
      employee.setLastName("Warner" + i);
      employee.setSalary(1000 * i);
      for (int j = i * 10; j < i * 10 + 3; j++) {
        System.out.print(" Account:" + j);
        Account account = new Account();
        account.setAid(j);
        account.setAccountNo("ACC" + i * j);
        account.setBranch("Branch" + i * j);
        account.setEmployee(employee);
        accounts.add(account);
      }
      System.out.println();
      employee.setAccounts(accounts);
      session.persist(employee);
      if (i % 5 == 0) { // same as the JDBC batch size
        session.flush(); // flush a batch of inserts and release memory:
        session.clear();
      }
    }
    transaction.commit();
    Employee Statement Queued: 1,  Account:10 Account:11 Account:12
    Employee Statement Queued: 2,  Account:20 Account:21 Account:22
    Employee Statement Queued: 3,  Account:30 Account:31 Account:32
    Employee Statement Queued: 4,  Account:40 Account:41 Account:42
    Employee Statement Queued: 5,  Account:50 Account:51 Account:52
    
    Hibernate: insert into Employee (firstName, lastName, salary, eid) values (?, ?, ?, ?)
    Name:, Connection:3, Time:3, Success:True
    Type:Prepared, Batch:True, QuerySize:1, BatchSize:1
    Query:["insert into Employee (firstName, lastName, salary, eid) values (?, ?, ?, ?)"]
    Params:[(David1,Warner1,1000.0,1)]
    
    Hibernate: insert into Account (accountNo, branch, employeeId, aid) values (?, ?, ?, ?)
    Hibernate: insert into Account (accountNo, branch, employeeId, aid) values (?, ?, ?, ?)
    Hibernate: insert into Account (accountNo, branch, employeeId, aid) values (?, ?, ?, ?)
    Name:, Connection:3, Time:2, Success:True
    Type:Prepared, Batch:True, QuerySize:1, BatchSize:3
    Query:["insert into Account (accountNo, branch, employeeId, aid) values (?, ?, ?, ?)"]
    Params:[(ACC10,Branch10,1,10),(ACC11,Branch11,1,11),(ACC12,Branch12,1,12)]
    ...
    ...
    ...
    Hibernate: insert into Employee (firstName, lastName, salary, eid) values (?, ?, ?, ?)
    Name:, Connection:3, Time:1, Success:True
    Type:Prepared, Batch:True, QuerySize:1, BatchSize:1
    Query:["insert into Employee (firstName, lastName, salary, eid) values (?, ?, ?, ?)"]
    Params:[(David10,Warner10,10000.0,10)]
    
    Hibernate: insert into Account (accountNo, branch, employeeId, aid) values (?, ?, ?, ?)
    Hibernate: insert into Account (accountNo, branch, employeeId, aid) values (?, ?, ?, ?)
    Hibernate: insert into Account (accountNo, branch, employeeId, aid) values (?, ?, ?, ?)
    Name:, Connection:3, Time:1, Success:True
    Type:Prepared, Batch:True, QuerySize:1, BatchSize:3
    Query:["insert into Account (accountNo, branch, employeeId, aid) values (?, ?, ?, ?)"]
    Params:[(ACC1000,Branch1000,10,100),(ACC1010,Branch1010,10,101),(ACC1020,Branch1020,10,102)]
    

    Notice the logs. All the employees go into the database in a separate batch. Similarly, accounts also go in 10 separate batches. So there is a total of 20 SQL INSERT statements executed in this process. To improve the performance, ideally all 10 employees should go in the database in a single SQL INSERT statement. And all the 30 accounts should go in the database in the second INSERT statement. So it should really take just 2 INSERT statements in the whole process. Hibernate provides hibernate.order_inserts property that can be used to force Hibernate to order inserts to allow for more batching. The official documentation cautions against it as a performance hit, so benchmark before and after to see if this actually helps or hurts our application.

    settings.put("hibernate.order_inserts", true);
    //or
    hibernate.order_inserts = true

    In Spring boot applications, we can force insert order using the following property.

    spring.jpa.properties.hibernate.order_inserts = true
    

    After configuring the the above property, for batch size 5, there will be 2 insert for employees(total 10) and 6 insert query for accounts(total 30).

    Similar to insert ordering, we can force hibernate to group SQL UPDATE statements in situations like the above.

    settings.put("hibernate.order_updates", "true");
    settings.put("hibernate.batch_versioned_data", "true");

    And if we are using Spring Boot, we need to add these to the application.properties: spring.jpa.properties.hibernate.order_updates=true spring.jpa.properties.hibernate.batch_versioned_data=true

    Batch Update: For retrieving and updating data, the same ideas apply. In addition, we need to use scroll() method to take advantage of server-side cursors for queries that return many rows of data. Lets first save the 10 employees then update using below code and see the logs.

    Transaction transaction = session.beginTransaction();
    Query<Employee> fromQuery = session.createQuery("from Employee", Employee.class);
    fromQuery.setCacheMode(CacheMode.IGNORE);
    fromQuery.scroll(ScrollMode.FORWARD_ONLY);
    ScrollableResults<Employee> scrollableResults = fromQuery.scroll();
    int count = 0;
    while (scrollableResults.next()) {
      ++count;
      Employee employee = scrollableResults.get();
      employee.setFirstName(employee.getFirstName() + count);
      employee.setLastName(employee.getLastName() + count);
      employee.setSalary(employee.getSalary() * count);
      if (count % 5 == 0) {
        session.flush();
        session.clear();
      }
    }
    transaction.commit();
    Hibernate: select e1_0.eid,e1_0.firstName,e1_0.lastName,e1_0.salary from Employee e1_0
    Name:, Connection:3, Time:1, Success:True
    Type:Prepared, Batch:False, QuerySize:1, BatchSize:0
    Query:["select e1_0.eid,e1_0.firstName,e1_0.lastName,e1_0.salary from Employee e1_0"]
    Params:[()]
    
    Hibernate: update Employee set firstName=?, lastName=?, salary=? where eid=?
    Hibernate: update Employee set firstName=?, lastName=?, salary=? where eid=?
    Hibernate: update Employee set firstName=?, lastName=?, salary=? where eid=?
    Hibernate: update Employee set firstName=?, lastName=?, salary=? where eid=?
    Hibernate: update Employee set firstName=?, lastName=?, salary=? where eid=?
    Name:, Connection:3, Time:2, Success:True
    Type:Prepared, Batch:True, QuerySize:1, BatchSize:5
    Query:["update Employee set firstName=?, lastName=?, salary=? where eid=?"]
    Params:[(David11,Warner11,1000.0,1),(David22,Warner22,4000.0,2),(David33,Warner33,9000.0,3),(David44,Warner44,16000.0,4),(David55,Warner55,25000.0,5)]
    
    Hibernate: update Employee set firstName=?, lastName=?, salary=? where eid=?
    Hibernate: update Employee set firstName=?, lastName=?, salary=? where eid=?
    Hibernate: update Employee set firstName=?, lastName=?, salary=? where eid=?
    Hibernate: update Employee set firstName=?, lastName=?, salary=? where eid=?
    Hibernate: update Employee set firstName=?, lastName=?, salary=? where eid=?
    Name:, Connection:3, Time:2, Success:True
    Type:Prepared, Batch:True, QuerySize:1, BatchSize:5
    Query:["update Employee set firstName=?, lastName=?, salary=? where eid=?"]
    Params:[(David66,Warner66,36000.0,6),(David77,Warner77,49000.0,7),(David88,Warner88,64000.0,8),(David99,Warner99,81000.0,9),(David1010,Warner1010,100000.0,10)]
    
  22. N+1 Problem

    By default Hibernate uses fetch mode as SELECT. It means, we have 2 associated entities(say Employee & Account) and we want to fetch the parent entity(Employee) from database then it fires 2 select queries. One is for the Employee & another is for Account.

    public class Employee implements Serializable {
      @Id
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      private int eid;
      private String firstName;
      private String lastName;
      private double salary;
      @OneToMany(cascade = CascadeType.PERSIST, mappedBy = "employee")
      private List<Account> accounts;
    }
    public class Account implements Serializable {
      @Id
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      private int aid;
      private String accountNo;
      private String branch;
      @ManyToOne(cascade = CascadeType.PERSIST)
      @JoinColumn(name = "employeeId")
      private Employee employee;
    }
    // to save an employee with 2 accounts
    Transaction transaction = session.beginTransaction();
    Employee employee = new Employee();
    employee.setFirstName("David");
    employee.setLastName("Warner");
    employee.setSalary(56789);
    Account account = new Account();
    account.setAccountNo("ACC");
    account.setBranch("Australia");
    account.setEmployee(employee);
    List<Account> accounts = new ArrayList<>();
    accounts.add(account);
    account = new Account();
    account.setAccountNo("BCC567");
    account.setBranch("New Zealand");
    accounts.add(account);
    employee.setAccounts(accounts);
    account.setEmployee(employee);
    session.persist(employee);
    transaction.commit();
    // To fetch the employee
    Employee employee = session.get(Employee.class, 1);
    System.out.println(employee);
    Hibernate: select e1_0.eid,e1_0.firstName,e1_0.lastName,e1_0.salary from Employee e1_0 where e1_0.eid=?
    Hibernate: select a1_0.employeeId,a1_0.aid,a1_0.accountNo,a1_0.branch from Account a1_0 where a1_0.employeeId=?
    
    Employee(eid=1, firstName=David, lastName=Warner, salary=56789.0, accounts=[Account(aid=1, accountNo=ACC, branch=Australia), Account(aid=2, accountNo=BCC567, branch=New Zealand)])

    As we can see here one select query is for parent and one is for child. Currently we have only one parent for these childs. Thats why, 1 select query for child table. If we had 100 parents then 100 times select query would have been executed for the childs. This is nothing but N+1 problem. Here N is the no of parent table. As many no of parent will be there, same no of the child select query will be executed. Lets see in the other example will multiple parents.

    // Save multiple employees
    Transaction transaction = session.beginTransaction();
    for (int i = 1; i <= 10; i++) {
      Employee employee = new Employee();
      employee.setFirstName("David");
      employee.setLastName("Warner");
      employee.setSalary(1000 * i);
      Account account = new Account();
      account.setAccountNo("ACC" + i);
      account.setBranch("Australia" + i);
      account.setEmployee(employee);
      List<Account> accounts = new ArrayList<>();
      accounts.add(account);
      account = new Account();
      account.setAccountNo("BCC567" + i);
      account.setBranch("New Zealand" + i);
      accounts.add(account);
      employee.setAccounts(accounts);
      account.setEmployee(employee);
      session.persist(employee);
    }
    transaction.commit();
    import org.hibernate.query.Query;
    String problemQuery = "FROM Employee";
    Query<Employee> fromQuery = session.createQuery(problemQuery, Employee.class);
    List<Employee> employees = fromQuery.list();
    for (Employee employee : employees) {
      List<Account> accounts = employee.getAccounts();
      System.out.println(employee);
      for (Account account : accounts) {
        System.out.println(account);
      }
      System.out.println();
    }
    Hibernate: select e1_0.eid,e1_0.firstName,e1_0.lastName,e1_0.salary from Employee e1_0
    Hibernate: select a1_0.employeeId,a1_0.aid,a1_0.accountNo,a1_0.branch from Account a1_0 where a1_0.employeeId=?
    Employee(eid=1, firstName=David, lastName=Warner, salary=56789.0, accounts=[Account(aid=1, accountNo=ACC1, branch=Australia1), Account(aid=2, accountNo=BCC5671, branch=New Zealand1)])
    Account(aid=1, accountNo=ACC1, branch=Australia1)
    Account(aid=2, accountNo=BCC5671, branch=New Zealand1)
    
    Hibernate: select a1_0.employeeId,a1_0.aid,a1_0.accountNo,a1_0.branch from Account a1_0 where a1_0.employeeId=?
    Employee(eid=2, firstName=David, lastName=Warner, salary=56789.0, accounts=[Account(aid=3, accountNo=ACC2, branch=Australia2), Account(aid=4, accountNo=BCC5672, branch=New Zealand2)])
    Account(aid=3, accountNo=ACC2, branch=Australia2)
    Account(aid=4, accountNo=BCC5672, branch=New Zealand2)
    ...
    ...
    Hibernate: select a1_0.employeeId,a1_0.aid,a1_0.accountNo,a1_0.branch from Account a1_0 where a1_0.employeeId=?
    Employee(eid=10, firstName=David, lastName=Warner, salary=56789.0, accounts=[Account(aid=19, accountNo=ACC5, branch=Australia5), Account(aid=20, accountNo=BCC5675, branch=New Zealand5)])
    Account(aid=19, accountNo=ACC5, branch=Australia5)
    Account(aid=20, accountNo=BCC5675, branch=New Zealand5)

    As we can see the logs, it generated 1 query for Employee & 10 queries for Account because we have 10 Employees.

    N+1 Solution: We can solve this by using FETCH strategy. We can use this either in HQL or Criteria. We can use one of the following HQL query to solve the N+1 problem.

    String query = "FROM Employee e LEFT JOIN FETCH e.accounts";
    String query = "FROM Employee e JOIN FETCH e.accounts";

    If we are using Criteria then we can set the JoinType.LEFT in the fetch() method.

    CriteriaBuilder criteriaBuilder = session.getCriteriaBuilder();
    CriteriaQuery<Employee> criteriaQuery = criteriaBuilder.createQuery(Employee.class);
    Root<Employee> rootEmployee = criteriaQuery.from(Employee.class);
    rootEmployee.fetch("accounts", JoinType.LEFT);
    CriteriaQuery<Employee> selectEmployee = criteriaQuery.select(rootEmployee);
    Query<Employee> query = session.createQuery(selectEmployee);
    List<Employee> employees = query.list();
    for (Employee employee : employees) {
      System.out.print(employee.getFirstName() + ": ");
      List<Account> accounts = employee.getAccounts();
      for (Account account : accounts) {
        System.out.print(account.getAccountNo() + " ");
      }
      System.out.println();
    }