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

Как работает Hibernate?  Начало здесь. Поговорим о извлечении, сохранении и удалении в Hibernate.

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

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

@Autowired
private SessionFactory sessionFactory

public List<Task> 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.

С параметром 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) Сохранение объектов

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

  1. Создаём 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(); // Создаём transient-object
    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);
       ...
    
    
  2. Добавляем объект в коллекцию дочерних объектов:
     
    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) Удаление объектов

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

  2.      Можно удалить и таким способом:
public void deleteTask(Long userId, Long taskId) {
   User user = (User) session.load(User.class, userId);
   user.getTasks().removeIf((Task task) -&amp;amp;gt; {
   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<Task> getTasks() {
    return tasks;
}

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

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

В третьей части мы поговорим о транзакциях.

(Visited 946 times, 1 visits today)

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

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