Как работает 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) Сохранение объектов
Давайте разберём несколько способов сохранения объекта-сущности в базу данных.
- Создаём 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); ...
- Добавляем объект в коллекцию дочерних объектов:
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) -&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, должны быть удалены из БД.
В третьей части мы поговорим о транзакциях.