Введение
В этой статье я хотел бы поделиться некоторыми соображениями и замечаниями по работе с Hibernate, касающихся сессий и транзакций. Я остановился на некоторых нюансах, которые возникают при начале освоения этой темы.
Библиотека Hibernate является самой популярной ORM-билиотекой и реализацией Java Persistence API. Часто используется как ORM-провайдер в обычных Java-приложениях, контейнерах сервлетов, в частности, в сервере приложений JBoss (и его потомке WildFly).
1) Объекты-сущности (Entity Objects)
Для примера возьмем две сущности — пользователь и его задачи:
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 )
Теперь приведем маппированные на данные сущности в БД классы-сущности:
@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<Task> getTasks() { return tasks; } public void setTasks(List<Task> 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; } }
Как мы можем видеть, для поля userId мы используем стратегию генерирования идентфикатора SEQUENCE
и для связи один-ко-многим используем аннотацию @OneToMany
.
@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; } }
Здесь для связи многие-ко-одному используется аннотация @ManyToOne
и @JoinColumn
. Об аннотациях JPA можно прочитать здесь.
2) Интерфейс Session (Сессия)
В Hibernate работа с БД осуществляется через интерфейс org.hibernate.Session
, который, согласно документации, является главным связующим интерфейсом между Java приложением и Hibernate. Он является центральным API классом, в основе которого лежит абстракция сервиса persistence.
Жизненный цикл Session привязан к началу и концу логической транзакции (Долгие транзакции могут охватывать несколько транзакций БД).
Главная функция Session — предоставить методы для создания, чтения и удаления для объектов классов, которые являются отображениями сущностей из БД. Объект типа Session получают из экземпляра типа org.hibernate.SessionFactory
, который должен присутствовать в приложении в виде singleton.
3) Состояния объектов
Объект-сущность может находиться в одном из 3-х состояний (статусов):
- transient object — объект-сущность была только инициализирована, но пока ещё не добавлена под управление ORM, не имеет представления в базе данных, ключевые поля, как правило, ещё не заданы.Поле Id не должно быть заполнено, иначе объект имеет статус detached;
- persistent object — объект в данном статусе — так называемая хранимая сущность, которая присоединена к конкретной сессии. Только в этом статусе объект взаимодействует с базой данных. При работе с объектом данного типа в рамках транзакции все изменения объекта записываются в базу;
- detached object — объект в данном статусе — это объект, отсоединённый от сессии, может существовать или не существовать в БД.
Любой объект-сущность можно переводить из одного статуса в другой. Для этого в интерфейсе Session существуют следующие методы:
- persist(Object) — преобразует объект из transient в persistent, то есть присоединяет к сессии и сохраняет в БД. Однако, если мы присвоим значение полю Id объекта, то получим PersistentObjectException — Hibernate посчитает, что объект detached, т. е. существует в БД. При сохранении метод persist() сразу выполняет INSERT, не делая SELECT.
- merge(Object) — преобразует объект из transient или detached в persistent. Если из transient, то работает аналогично persist()(генерирует для объекта новый Id, даже если он задан), если из detached — загружает объект из БД, присоединяет к сессии, а при сохранении выполняет запрос update
- replicate(Object, ReplicationMode) — преобразует объект из detached в persistent, при этом у объекта обязательно должен быть заранее установлен Id. Данный метод предназначен для сохранения в БД объекта с заданным Id, чего не позволяют сделать persist() и merge(). Если объект с данным Id уже существует в БД, то поведение определяется согласно правилу из перечисления
org.hibernate.ReplicationMode
:
— ReplicationMode.IGNORE — ничего не меняется в базе.
— ReplicationMode.OVERWRITE — объект сохраняется в базу вместо существующего.
— ReplicationMode.LATEST_VERSION — в базе сохраняется объект с последней версией.
— ReplicationMode.EXCEPTION — генерирует исключение. - delete(Object) — удаляет объект из БД, иными словами, преобразует persistent в transient. Object может быть в любом статусе, главное, чтобы был установлен Id.
- save(Object) — сохраняет объект в БД, генерируя новый Id, даже если он установлен. Object может быть в статусе transient или detached
- update(Object) — обновляет объект в БД, преобразуя его в persistent (Object в статусе detached)
- saveOrUpdate(Object) — вызывает save() или update()
- refresh(Object) — обновляет detached-объект, выполнив select к БД, и преобразует его в persistent
- get(Object.class, id) — получает из БД объект класса-сущности с определённым Id в статусе persistent
Более подробно почитать о переходах между состояниями можно здесь.
Объект Session кэширует у себя загруженные объекты; при загрузке объекта из БД в первую очередь проверяется кэш. Для того, чтобы удалить объект из кэша и отсоединить от сессии, используется session.evict(Object). Метод session.clear() применит evict() ко всем объектам в сессии.
А теперь обратим внимание на аннотации @OneToMany и @ManyToOne в классах-сущностях. Параметр fetch в @OneToMany обозначает, когда загружать дочерние объекты. Может иметь одно из двух значений, указанных в перечислении javax.persistence.FetchType:
FetchType.EAGER
— загружать коллекцию дочерних объектов сразу же, при загрузке родительских объектов.
FetchType.LAZY
— загружать коллекцию дочерних объектов при первом обращении к ней (вызове get) — так называемая отложенная загрузка.
Параметр cascade обозначает, какие из методов интерфейса Session будут распространяться каскадно к ассоциированным сущностям. Например, в классе-сущности User для коллекции tasks укажем:
@OneToMany(fetch = FetchType.LAZY, mappedBy = "user", cascade = {CascadeType.PERSIST, CascadeType.MERGE}) public List<Task> getTasks() { return tasks; } public void setTasks(List<Task> tasks) { this.tasks = tasks; }
Тогда при выполнении session.persist(user)
или session.merge(user)
операции persist или merge будут применены ко всем объектам из tasks. Аналогично для остальных операций из перечисления javax.persistence.CascadeType
. CascadeType.ALL
применяет все операции из перечисления. Необходимо правильно настроить CascadeType, дабы не подгружать из базы кучу лишних ассоциированных объектов-сущностей.
Продолжение статьи здесь.