📜 ⬆️ ⬇️

Hibernate. Basic principles of working with sessions and transactions

In my first article on Habré, I would like to share some thoughts and comments on working with Hibernate regarding sessions and transactions. I stopped at some of the nuances that arise at the beginning of the development of this topic. I admit, while Junior programmer himself, with Hibernate did not work constantly, therefore, as always , mistakes are possible, if you notice these, I will be grateful for the amendments.

Hibernate is the most popular ORM library and a Java Persistence API implementation. It is often used as an ORM provider in regular Java applications, servlet containers, in particular, in the JBoss application server (and its descendant WildFly).

Let's start, perhaps.

one). Entity Objects

Consider two entities - the user and his task:
')
CREATE TABLE "user" ( user_id serial NOT NULL, login character varying(10), password character varying(10), role integer, name character varying(20) NOT NULL, CONSTRAINT user_pkey PRIMARY KEY (user_id) ) CREATE TABLE task ( task_id serial NOT NULL, user_id bigint, task_date date, name character varying(20), definition character varying(200), CONSTRAINT tasks_pkey PRIMARY KEY (task_id), CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES "user" (user_id) MATCH SIMPLE ON UPDATE NO ACTION ON DELETE NO ACTION ) 

Now we give the entity classes for these tables:

 @Entity @Table(name = "user", schema = "public") public class User { private Long userId; private String name; private String login; private String password; private Integer role; private List<Task> tasks; @Id @SequenceGenerator(name = "user_seq", sequenceName = "user_user_id_seq", allocationSize = 0) @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "user_seq") @Column(name = "user_id") public Long getUserId() { return userId; } public void setUserId(Long userId) { this.userId = userId; } @OneToMany(fetch = FetchType.LAZY, mappedBy = "user", cascade = CascadeType.ALL) public List<Tasks> getTasks() { return tasks; } public void setTasks(List<Tasks> tasks) { this.tasks = tasks; } @Column(name = "name") public String getName() { return name; } public void setName(String name) { this.name = name; } @Column(name = "login") public String getLogin() { return login; } public void setLogin(String login) { this.login = login; } @Column(name = "password") public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } } @Entity @Table(name = "task", schema = "public") public class Task { private Long taskId; private User user; private Date taskDate; private String name; private String definition; private Priority priority; private Type type; @Id @SequenceGenerator(name = "tasks_seq", sequenceName = "tasks_task_id_seq", allocationSize = 0) @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "tasks_seq") @Column(name = "task_id", unique = true, nullable = false) public Long getTaskId() { return taskId; } public void setTaskId(Long taskId) { this.taskId = taskId; } @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL) @JoinColumn(name = "user_id") public User getUser() { return user; } public void setUser(User user) { this.user = user; } @Column(name = "task_date") public Date getTaskDate() { return taskDate; } public void setTaskDate(Date taskDate) { this.taskDate = taskDate; } @Column(name = "name") public String getName() { return name; } public void setName(String name) { this.name = name; } @Column(name = "definition") public String getDefinition() { return definition; } public void setDefinition(String definition) { this.definition = definition; } } 

JPA annotations can be found here .

2). Session Interface

In Hibernate, working with a database is done through an object of type org.hibernate.Session .
Excerpt from the documentation:
The main runtime interface between a Java application and Hibernate. This is a central API.
Session is bounded by the beginning of the logical transaction. (Long transactions might span several database transactions.)
For the mapped entity classes.

The org.hibernate.Session interface is the bridge between the application and Hibernate. With the help of sessions, all CRUD operations with entity objects are performed. An object of type Session is obtained from an instance of type org.hibernate.SessionFactory , which must be present in the application as a singleton .

3). Object states

An entity object can be in one of 3 states (statuses):


Any entity object can be transferred from one status to another. For this, the following methods exist in the Session interface:


The Session object caches the loaded objects; When an object is loaded from the database, the cache is first checked. To remove an object from the cache and disconnect from the session, use session.evict (Object) . The session.clear () method will apply evict () to all objects in the session.

And now let's pay attention to the annotations @OneToMany and @ManyToOne in entity classes. The fetch parameter in @OneToMany indicates when to load child objects. It can have one of the two values ​​specified in the javax.persistence.FetchType enumeration:

FetchType.EAGER - load a collection of child objects immediately when loading parent objects.
FetchType.LAZY - load a collection of child objects when it is first accessed (call get ) - the so-called delayed loading.

The cascade parameter indicates which of the methods of the Session interface will be cascaded to the associated entities. For example, in the User entity class for the tasks collection, we specify:

 @OneToMany(fetch = FetchType.LAZY, mappedBy = "user", cascade = {CascadeType.PERSIST, CascadeType.MERGE}) public List<Tasks> getTasks() { return tasks; } public void setTasks(List<Tasks> tasks) { this.tasks = tasks; } 

Then, when running session.persist (user) or session.merge (user) , the persist or merge operations will be applied to all objects from tasks . Similarly for the remaining operations from the javax.persistence.CascadeType enumeration. CascadeType.ALL applies all operations from the enumeration. You need to properly configure CascadeType in order not to load a bunch of unnecessary associated entity objects from the database.

four). Extracting objects from the database

Let's give a simple example:

  @Autowired private SessionFactory sessionFactory public void getTasks(Long userId) { ... Session session = sessionFactory.openSession(); User user = (User) session.load(User.class, userId); Session session = sessionFactory.openSession(); List<Task> tasksList = user.getTasks(); ... } 

Instead of the session.get () method, you can use session.load () . The session.load () method returns the so-called proxy-object . Proxy-object is a mediation object through which we can interact with a real object in the database. It expands the functionality of the entity object. The interaction with the proxy-object is completely analogous to the interaction with the entity object. Proxy-object differs from the entity object in that when creating a proxy-object, not a single query to the database is executed, that is, Hibernate simply believes that the object with the given Id exists in the database. However, the proxy object first called get or set immediately triggers a select request, and if the object with the given Id is not in the database, then we will get an ObjectNotFoundException . The main purpose of the proxy-object is to implement delayed loading.

Calling user.getTasks () initiates the loading of user tasks from the database, since the User class for tasks is set to FetchType.LAZY .

LazyInitializationException

With the parameter FetchType.LAZY need to be careful. Sometimes when loading associated entities, we can catch a LazyInitializationException exception. In the above code, during a call to user.getTasks (), the user must be either persistent or proxy .

Also, LazyInitializationException may cause a slight change in our code:

 public List<Task> getTasks(Long userId) { ... Session session = sessionFactory.openSession(); User user = (User) session.load(User.class, userId); List<Task> tasksList = user.getTasks(); session.close(); return tasksList; } 

Here, theoretically, everything is correct. But when trying to access tasksList, we CAN get a LazyInitializationException . But in the debugger, this code executes correctly. Why? Because user.getTasks () only returns a link to the collection, but does not wait for it to load. Without waiting for the data to load, we closed the session. Exit - perform in a transaction, i.e .:

  public List<Task> getTasks(Long userId) { ... User user = (User) session.load(User.class, userId); Session session = sessionFactory.openSession(); session.beginTransaction(); List<Task> tasksList = user.getTasks(); session.getTransaction().commit(); return tasksList; } 

Sample with conditions

And now we give some simple examples of data sampling with conditions. To do this, Hibernate uses objects of type org.hibernate.Criteria :

  public List<Task> getUser(String login) { ... Session session = sessionFactory.openSession(); Criteria userCriteria = session.createCriteria(User.class); userCriteria.add(Restrictions.eq("login", login)); user = (User) userCriteria.uniqueResult(); session.close(); ... } 

Here it is clear that we execute select * from user where login = 'login' . In the add method, we pass an object of type Criterion , representing a certain selection criterion. The org.hibernate.criterion.Restrictions class provides many different kinds of criteria. The “login” parameter indicates the name of the property of the class-entity, and not the field in the database table.
Here are a couple of examples:

but).
  public List<Task> getTasksByName(String name) { ... session = sessionFactory.openSession(); Criteria criteria = session.createCriteria(Task.class); List<Task> tasks = criteria.add(Restrictions.like("name", name, MatchMode.ANYWHERE)).list(); ... } 

Here we select the contents of the name property of the Task class entity. MatchMode.ANYWHERE means that you need to search for the substring name anywhere in the name property.

b).
And here we get 50 lines, starting from the 20th number in the table.

  public List<Task> getTasks() { ... Session session = sessionFactory.openSession(); Criteria criteria = session.createCriteria(Task.class); List<Task> tasks = criteria.setFirstResult(20).setMaxResults(50).list(); ... } 

five). Saving objects

Let's look at several ways to save an entity object to the database.

but). Create a transient-object and save it to the database:

  @Autowired private UserDao userDao; @Autowired private SessionFactory sessionFactory; public void saveUser(String login) { User user = userDao.getUserByLogin(login); Session session = sessionFactory.openSession(); session.openTransaction(); Task task = new Task(); task.setName(" 1"); task.setDefinition(" 1"); task.setTaskDate(new Date()); task.setUser(user); session.saveOrUpdate(task); session.flush(); session.getTransaction().commit(); return task.getTaskId(); } 

We note a few nuances. First, saving to the database can only be done as part of a transaction. Calling session.openTransaction () opens a new transaction for the session, and session.getTransaction (). Commit () performs it. Secondly, in the task.setUser (user) method we pass user in the detached state. You can also transfer in the status persistent .

This code will execute (not counting receiving user ) 2 queries - select nextval ('task_task_id_seq') and insert into task ...
Instead of saveOrUpdate (), you can perform save () , persist () , merge () - there will also be 2 requests. The call session.flush () applies all changes to the database, but, to be honest, this call is useless here, since nothing is stored in the database until commit () , which itself will cause flush () .

Remember that if we inside the transaction change something in the persistent or proxy-object status object loaded from the database, the update request will be executed. If the task should refer to the new user , then do this:

  User user = new User(); //  <i>transient-object</i> user.setLogin("user"); user.setPassword("user"); ... task.setUser(user); session.saveOrUpdate(task); //  

Attention: in the Task class for the user field, CascadeType.PERSIST , CascadeType.MERGE or CascadeType.ALL must be set.

If we have userId with an existing user in the database, then we do not need to load the User object from the database, making an extra select . Since we cannot assign the user ID directly to the property of the Task class, we need to create an object of the User class with only userId values . Naturally, this cannot be a transient-object , so here you should use a proxy object known to us.

 public void saveTask(Long userId, Task task) ... task.setUser((User) session.load(User.class, userId)); //       session.saveOrUpdate(task); ... 

b). Add an object to the collection of child objects:

  public Long saveUser(String login) { Session session = sessionFactory.openSession(); session.openTransaction(); user = (User) session.load(User.class, userId); Task task = new Task(); task.setName(""); task.setUser(user); user.getTasks().add(task); session.getTransaction().commit(); return user.getUserId(); } 

In User, the tasks property should be CascadeType.ALL . If it is CascadeType.MERGE , then after user.getTasks (). Add (task) execute session.merge (user) . This code will execute 3 queries - select * from user , select nextval ('task_task_id_seq') and insert into task ...

6). Deleting objects

but). You can delete it by creating a transient-object :

  public void deleteTask(Long taskId) { Session session = sessionFactory.openSession(); session.openTransaction(); Tasks task = new Tasks(); task.setTaskId(taskId); session.delete(task); session.getTransaction().commit(); } 

This code will delete only the task . However, if the task is an object of the proxy , persistent or detached type and CascadeType.REMOVE operates in the Task class for the user field, then the associated user will also be deleted from the database. If you do not need to delete the user, do what? Correct, task.setUser (null)

b). You can delete in this way:

  public void deleteTask(Long userId, Long taskId) { User user = (User) session.load(User.class, userId); user.getTasks().removeIf((Task task) -> { if (task.getTaskId() == taskId) { task.setUser(null); return true; } else return false; }); } 

This code simply removes the connection between the task and the user . Here we have applied the newfangled lambda expression . The task object will be removed from the database under one condition - if you change something in the User entity class:

  @OneToMany(fetch = FetchType.LAZY, mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) public List<Tasks> getTasks() { return tasks; } public void setTasks(List<Tasks> tasks) { this.tasks = tasks; } 

The orphanRemoval = true parameter indicates that all Task objects that do not have a User reference should be removed from the database.

7). Declarative transaction management

For declarative transaction management, we will use the Spring Framework . Transaction management is carried out through a transaction manager. Instead of calling session.openTransaction () and session.commit () , the @Transactional annotation is used . The following must be present in the application configuration:

  <bean id="sessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean"> <property name="configLocation" value="classpath:hibernate.cfg.xml"></property> </bean> <bean id="transactionManager" class="org.springframework.orm.hibernate4.HibernateTransactionManager"> <property name="sessionFactory" ref="sessionFactory"/> </bean> <tx:annotation-driven transaction-manager="transactionManager"/> 

Here we have defined the transactionManager bean to which the sessionFactory is attached . The HibernateTransactionManager class is an implementation of the common org.springframework.transaction.PlatformTransactionManager interface for the Hibernate SessionFactory library. annotation-driven instructs the transaction manager to process the @Transactional annotation.

- Chatter is worth nothing. Show me the code. (Linus Torvalds)

  @Transactional(propagation = Propagation.REQUIRED, rollbackFor = {ObjectNotFoundException.class, ConstraintViolationException.class}) public Long saveTask(Long userId) { Session session = sessionFactory.getCurrentSession(); Tasks task = new Tasks(); task.setName(" 1"); task.setDefinition(" 1"); task.setTaskDate(new Date()); task.setUser((User) session.load(User.class, userId)); session.saveOrUpdate(task); return task.getTaskId(); } 

The @Transactional annotation indicates that the method should be executed in a transaction. The transaction manager opens a new transaction and creates an instance of Session for it, which is accessible via sessionFactory.getCurrentSession () . All methods that are called in the method with this annotation also have access to this transaction, because the Session instance is a thread variable (ThreadLocal). Calling sessionFactory.openSession () will open a completely different session that is not related to the transaction.

The rollbackFor parameter specifies the exceptions that should be rolled back when a rollout is thrown. There is a reverse parameter - noRollbackFor , indicating that all exceptions, except those listed, lead to a rollback of the transaction.

The propagation parameter is the most interesting. It specifies the principle of distribution of the transaction. Can be any value from the org.springframework.transaction.annotation.Propagation enumeration. Let's give an example:

  @Autowired private SessionFactory sessionFactory; @Autowired private UserDao userDao; @Transactional(propagation = Propagation.REQUIRED, rollbackFor = {ConstraintViolationException.class}) public Long saveTask(Long userId) { Session session = sessionFactory.getCurrentSession(); User user = userDao.getUserByLogin("user1"); Tasks task = new Tasks(); task.setName(" 1"); ... task.setUser(user); session.saveOrUpdate(task); return task.getTaskId(); } 

The UserDao.getUserByLogin () method can also be annotated with @Transactional . And here the propagation parameter determines the behavior of the UserDao.getUserByLogin () method regarding the saveTask () method transaction:


Good stuff about transactions . It should be remembered that the use of transactions carries additional costs in performance.

Well, let's summarize

In my article I covered the most basic principles of working with sessions and transactions in Hibernate. I hope that this article will be useful for novice Java programmers in overcoming the first threshold in learning the superclass (not for everyone, perhaps) Hibernate library. I wish you all success in our complex and interesting programming activities!

Sample project .

Thanks for attention!

Source: https://habr.com/ru/post/271115/


All Articles