Hibernate. Сессия и транзакции: как это работает? (Часть 1)

Введение

В этой статье я хотел бы поделиться некоторыми соображениями и замечаниями по работе с 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, дабы не подгружать из базы кучу лишних ассоциированных объектов-сущностей.

Продолжение статьи здесь.

(Visited 1 443 times, 1 visits today)

Добавить комментарий

Этот сайт использует Akismet для борьбы со спамом. Узнайте как обрабатываются ваши данные комментариев.