Pull to refresh

Hibernate. Основные принципы работы с сессиями и транзакциями

Reading time 14 min
Views 239K
В моей первой статье на Хабре я хотел бы поделиться некоторыми соображениями и замечаниями по работе с Hibernate, касающихся сессий и транзакций. Я остановился на некоторых нюансах, которые возникают при начале освоения этой темы. Признаюсь, сам пока Junior-программист, с 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<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 можно прочитать здесь.

2). Интерфейс Session

В Hibernate работа с БД осуществляется через объект типа org.hibernate.Session.
Выдержка из документации:
The main runtime interface between a Java application and Hibernate. This is the central API class abstracting the notion of a persistence service.
The lifecycle of a Session is bounded by the beginning and end of a logical transaction. (Long transactions might span several database transactions.)
The main function of the Session is to offer create, read and delete operations for instances of mapped entity classes.

Интерфейс org.hibernate.Session является мостом между приложением и Hibernate. С помощью сессий выполняются все CRUD-операции с объектами-сущностями. Объект типа Session получают из экземпляра типа org.hibernate.SessionFactory, который должен присутствовать в приложении в виде singleton.

3). Состояния объектов

Объект-сущность может находиться в одном из 3-х состояний (статусов):

  • transient object. Объекты в данном статусе — это заполненные экземпляры классов-сущностей. Могут быть сохранены в БД. Не присоединены к сессии. Поле 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<Tasks> getTasks() {
        return tasks;
    }

    public void setTasks(List<Tasks> tasks) {
        this.tasks = tasks;
    }

Тогда при выполнении session.persist(user) или session.merge(user) операции persist или merge будут применены ко всем объектам из tasks. Аналогично для остальных операций из перечисления javax.persistence.CascadeType. CascadeType.ALL применяет все операции из перечисления. Необходимо правильно настроить CascadeType, дабы не подгружать из базы кучу лишних ассоциированных объектов-сущностей.

4). Извлечение объектов из БД

Приведём простой пример:

     @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();
         ...
      }

Вместо метода session.get() можно использовать session.load(). Метод session.load() возвращает так называемый proxy-object. Proxy-object — это объект-посредник, через который мы можем взаимодействовать с реальным объектом в БД. Он расширяет функционал объекта-сущности. Взаимодействие с proxy-object полностью аналогично взаимодействию с объектом-сущностью. Proxy-object отличается от объекта-сущности тем, что при создании proxy-object не выполняется ни одного запроса к БД, т. е. Hibernate просто верит нам, что объект с данным Id существует в БД. Однако первый вызванный get или set у proxy-object сразу инициирует запрос select, и если объекта с данным Id нет в базе, то мы получим ObjectNotFoundException. Основное предназначение proxy-object — реализация отложенной загрузки.

Вызов user.getTasks() инициирует загрузку задач юзера из БД, так как в классе User для tasks установлен FetchType.LAZY.

LazyInitializationException

С параметром FetchType.LAZY нужно быть аккуратнее. Иногда при загрузке ассоциированных сущностей мы можем поймать исключение LazyInitializationException. В вышеуказанном коде во время вызова user.getTasks() user должен быть либо в статусе persistent, либо proxy.

Также LazyInitializationException может вызвать небольшое изменение в нашем коде:

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;
}

Здесь теоретически всё верно. Но при попытке обращения к tasksList мы МОЖЕМ получить LazyInitializationException. Но в дебагере данный код отрабатывает верно. Почему? Потому, что user.getTasks() только возвращает ссылку на коллекцию, но не ждёт её загрузки. Не подождав, пока загрузятся данные, мы закрыли сессию. Выход — выполнять в транзакции, т. е.:

    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;
    }

Выборка с условиями

А теперь приведём несколько простых примеров выборки данных с условиями. Для этого в Hibernate используются объекты типа 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();
            ...
       }

Здесь понятно, что мы выполняем select * from user where login='login'. В метод add мы передаём объект типа Criterion, представляющий определённый критерий выборки. Класс org.hibernate.criterion.Restrictions предоставляет множество различных видов критериев. Параметр «login» обозначает название свойства класса-сущности, а не поля в таблице БД.
Приведём ещё пару примеров:

а).
   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();
       ...
   }

Здесь мы выбираем по содержимому свойства name класса-сущности Task. MatchMode.ANYWHERE означает, что нужно искать подстроку name в любом месте свойства «name».

б).
А здесь мы получаем 50 строк, начиная с 20-го номера в таблице.

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

5). Сохранение объектов

Давайте разберём несколько способов сохранения объекта-сущности в базу данных.

а). Создаём transient-object и сохраняем в базу:

    @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();
    }

Отметим несколько нюансов. Во-первых, сохранение в БД можно производить только в рамках транзакции. Вызов session.openTransaction() открывает для данной сессии новую транзакцию, а session.getTransaction().commit() её выполняет. Во-вторых, в метод task.setUser(user) мы передаём user в статусе detached. Можно передать и в статусе persistent.

Данный код выполнит (не считая получения user) 2 запроса — select nextval('task_task_id_seq') и insert into task...
Вместо saveOrUpdate() можно выполнить save(), persist(), merge() — будет также 2 запроса. Вызов session.flush() применяет все изменения к БД, но, если честно, этот вызов здесь бесполезен, так как ничего не сохраняется в БД до commit(), который сам вызовет flush().

Помним, что если мы внутри транзакции что-то изменим в загруженном из БД объекте статуса persistent или proxy-object, то выполнится запрос update. Если task должен ссылаться на нового user, то делаем так:

         User user = new User();         // Создаём <i>transient-object</i>
         user.setLogin("user");
         user.setPassword("user");
         ...
         task.setUser(user);
         session.saveOrUpdate(task); // Сохраняем

Внимание: в классе Task для поля user должен быть установлен CascadeType.PERSIST, CascadeType.MERGE или CascadeType.ALL.

Если мы имеем на руках userId существующего в БД юзера, то нам не обязательно загружать объект User из БД, делая лишний select. Так как мы не можем присвоить ID юзера непосредственно свойству класса Task, нам нужно создать объект класса User с единственно заполненными userId. Естественно, это не может быть transient-object, поэтому здесь следует воспользоваться известным нам proxy-объектом.

public void saveTask(Long userId, Task task)
         ...
         task.setUser((User) session.load(User.class, userId));    // Никакого запроса к БД не происходит
         session.saveOrUpdate(task);
         ...

б). Добавляем объект в коллекцию дочерних объектов:

    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();
    }

В User для свойства tasks должен стоять CascadeType.ALL. Если стоит CascadeType.MERGE, то после user.getTasks().add(task) выполнить session.merge(user). Данный код выполнит 3 запроса — select * from user, select nextval('task_task_id_seq') и insert into task

6). Удаление объектов

а). Можно удалить, создав 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();
      }

Данный код удалит только task. Однако, если task — объект типа proxy, persistent или detached и в классе Task для поля user действует CascadeType.REMOVE, то из базы удалится также ассоциированный user. Если удалять юзера не нужно, выполнить что? Правильно, task.setUser(null)

б). Можно удалить и таким способом:

         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;
            });
         }

Данный код просто удаляет связь между task и user. Здесь мы применили новомодное лямбда-выражение. Объект task удалится из БД при одном условии — если изменить кое-что в классе-сущности User:

    @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;
    }

Параметр orphanRemoval = true указывает, что все объекты Task, которые не имеют ссылки на User, должны быть удалены из БД.

7). Декларативное управление транзакциями

Для декларативного управления транзакциями мы будем использовать Spring Framework. Управление транзакциями осуществляется через менеджер транзакций. Вместо вызовов session.openTransaction() и session.commit() используется аннотация @Transactional. В конфигурации приложения должно присутствовать следующее:

    <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"/>

Здесь мы определили бин transactionManager, к которому привязан бин sessionFactory. Класс HibernateTransactionManager является реализацией общего интерфейса org.springframework.transaction.PlatformTransactionManager для SessionFactory библиотеки Hibernate. annotation-driven указывает менеджеру транзакций обрабатывать аннотацию @Transactional.

— Болтовня ничего не стоит. Покажите мне код. (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();
    }

Аннотация @Transactional указывает, что метод должен выполняться в транзакции. Менеджер транзакций открывает новую транзакцию и создаёт для неё экземпляр Session, который доступен через sessionFactory.getCurrentSession(). Все методы, которые вызываются в методе с данной аннотацией, также имеют доступ к этой транзакции, потому что экземпляр Session является переменной потока (ThreadLocal). Вызов sessionFactory.openSession() откроет совсем другую сессию, которая не связана с транзакцией.

Параметр rollbackFor указывает исключения, при выбросе которых должен быть произведён откат транзакции. Есть обратный параметр — noRollbackFor, указывающий, что все исключения, кроме перечисленных, приводят к откату транзакции.

Параметр propagation самый интересный. Он указывает принцип распространения транзакции. Может принимать любое значение из перечисления org.springframework.transaction.annotation.Propagation. Приведём пример:

    @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();
    }

Метод UserDao.getUserByLogin() также может быть помечен аннотацией @Transactional. И здесь параметр propagation определит поведение метода UserDao.getUserByLogin() относительно транзакции метода saveTask():

  • Propagation.REQUIRED — выполняться в существующей транзакции, если она есть, иначе создавать новую.
  • Propagation.MANDATORY — выполняться в существующей транзакции, если она есть, иначе генерировать исключение.
  • Propagation.SUPPORTS — выполняться в существующей транзакции, если она есть, иначе выполняться вне транзакции.
  • Propagation.NOT_SUPPORTED — всегда выполняться вне транзакции. Если есть существующая, то она будет остановлена.
  • Propagation.REQUIRES_NEW — всегда выполняться в новой независимой транзакции. Если есть существующая, то она будет остановлена до окончания выполнения новой транзакции.
  • Propagation.NESTED — если есть текущая транзакция, выполняться в новой, так называемой, вложенной транзакции. Если вложенная транзакция будет отменена, то это не повлияет на внешнюю транзакцию; если будет отменена внешняя транзакция, то будет отменена и вложенная. Если текущей транзакции нет, то просто создаётся новая.
  • Propagation.NEVER — всегда выполнять вне транзакции, при наличии существующей генерировать исключение.

Хороший материал о транзакциях. Следует помнить, что использование транзакций несёт дополнительные издержки в производительности.

Ну что ж, подведём итоги

В моей статье я осветил самые основные принципы работы с сессиями и транзакциями в Hibernate. Надеюсь, что начинающим Java-программистам статья будет полезна при преодолении первого порога в изучении суперклассной (не для всех, возможно) библиотеки Hibernate. Желаю всем успехов в нашей сложной и интересной программерской деятельности!

Пример проекта.

Спасибо за внимание!
Tags:
Hubs:
+8
Comments 9
Comments Comments 9

Articles