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):
- transient object . Objects in this status are completed instances of entity classes. Can be saved in db. Not attached to the session. The Id field must not be filled, otherwise the object has a detached status;
- persistent object . The object in this status is the so-called stored entity, which is attached to a specific session. Only in this status the object interacts with the database. When working with an object of this type as part of a transaction, all changes to the object are recorded in the database;
- detached object . An object in this status is an object disconnected from a session, it may or may not exist in the database.
Any entity object can be transferred from one status to another. For this, the following methods exist in the
Session interface:
- persist (Object) - converts an object from transient to persistent , that is, attaches to the session and saves it to the database. However, if we assign a value to the object's Id field, we get PersistentObjectException - Hibernate will consider that the object is detached , i.e. it exists in the database. When saving, the persist () method immediately executes the insert , without making a select .
- merge (Object) - converts an object from transient or detached to persistent . If from transient , it works in the same way as persist () (generates a new Id for an object, even if it is specified), if from detached - loads an object from the database, attaches to the session, and when saved, performs an update request
- replicate (Object, ReplicationMode) - converts an object from detached to persistent , while the object must have an Id pre-set. This method is intended to save an object with the specified Id in the database, which is not possible to do persist () and merge () . If an object with this Id already exists in the database, then the behavior is determined according to the rule from the org.hibernate.ReplicationMode enumeration:
ReplicationMode.IGNORE - nothing changes in the database.
ReplicationMode.OVERWRITE - the object is saved to the database instead of the existing one.
ReplicationMode.LATEST_VERSION - the object with the latest version is saved in the database.
ReplicationMode.EXCEPTION - generates an exception. - delete (Object) - deletes an object from the database, in other words, converts persistent to transient . Object can be in any status, as long as Id is set.
- save (Object) - saves the object in the database, generating a new Id , even if it is installed. Object can be transient or detached
- update (Object) - updates the object in the database, transforming it into persistent ( Object in the detached status)
- saveOrUpdate (Object) - calls save () or update ()
- refresh (Object) - updates the detached object by executing select to the database and converts it to persistent
- get (Object.class, id) - gets from the database an object of an entity class with a specific Id in the persistent status
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();
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));
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:
- Propagation.REQUIRED - run in an existing transaction, if one exists, otherwise create a new one.
- Propagation.MANDATORY - run in an existing transaction, if one exists, otherwise generate an exception.
- Propagation.SUPPORTS - run in an existing transaction, if it exists, otherwise run outside the transaction.
- Propagation.NOT_SUPPORTED - always executed outside the transaction. If there is an existing one, then it will be stopped.
- Propagation.REQUIRES_NEW - always run in a new, independent transaction. If there is an existing one, then it will be stopped until the completion of a new transaction.
- Propagation.NESTED - if there is a current transaction, run in a new, so-called, nested transaction. If the nested transaction is canceled, it will not affect the external transaction; if an external transaction is canceled, the nested one will be canceled. If there is no current transaction, then a new one is simply created.
- Propagation.NEVER - always execute outside the transaction, if there is an existing one, generate an exception.
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!