Шаблон DAO в Java

1. Обзор

Шаблон объекта доступа к данным (DAO) - это структурный шаблон, который позволяет нам изолировать уровень приложения / бизнеса от уровня сохраняемости (обычно это реляционная база данных, но это может быть любой другой механизм сохраняемости) с помощью абстрактного API .

Функциональность этого API заключается в том, чтобы скрыть от приложения все сложности, связанные с выполнением операций CRUD в базовом механизме хранения. Это позволяет обоим слоям развиваться отдельно, ничего не зная друг о друге.

В этом руководстве мы глубоко погрузимся в реализацию шаблона и узнаем, как использовать его для абстрагирования вызовов к менеджеру сущностей JPA.

2. Простая реализация

Чтобы понять, как работает шаблон DAO, давайте создадим базовый пример.

Допустим, мы хотим разработать приложение, управляющее пользователями. Чтобы модель предметной области приложения полностью не зависела от базы данных, мы создадим простой класс DAO, который позаботится о том, чтобы эти компоненты были аккуратно отделены друг от друга .

2.1. Доменный класс

Поскольку наше приложение будет работать с пользователями, нам нужно определить только один класс для реализации его модели предметной области:

public class User { private String name; private String email; // constructors / standard setters / getters }

Класс User - это простой контейнер для пользовательских данных, поэтому он не реализует никакого другого поведения, заслуживающего внимания.

Конечно, наиболее актуальный выбор дизайна, который нам здесь нужно сделать, - это как сохранить приложение, использующее этот класс, изолированным от любого механизма сохранения, который может быть реализован в какой-то момент.

Что ж, это именно та проблема, которую пытается решить шаблон DAO.

2.2. API DAO

Давайте определим базовый уровень DAO, чтобы мы могли увидеть, как он может полностью отделить модель предметной области от уровня сохраняемости.

Вот API DAO:

public interface Dao { Optional get(long id); List getAll(); void save(T t); void update(T t, String[] params); void delete(T t); }

С высоты птичьего полета , это ясно видеть , что ДАО интерфейс определяет абстрактный интерфейс API , который выполняет CRUD операций над объектами типа T .

Благодаря высокому уровню абстракции, который предоставляет интерфейс, легко создать конкретную детализированную реализацию, которая работает с объектами User .

2.3. UserDao Класс

Давайте определим индивидуальную реализацию интерфейса Dao :

public class UserDao implements Dao { private List users = new ArrayList(); public UserDao() { users.add(new User("John", "[email protected]")); users.add(new User("Susan", "[email protected]")); } @Override public Optional get(long id) { return Optional.ofNullable(users.get((int) id)); } @Override public List getAll() { return users; } @Override public void save(User user) { users.add(user); } @Override public void update(User user, String[] params) { user.setName(Objects.requireNonNull( params[0], "Name cannot be null")); user.setEmail(Objects.requireNonNull( params[1], "Email cannot be null")); users.add(user); } @Override public void delete(User user) { users.remove(user); } }

Класс UserDao реализует все функции, необходимые для выборки, обновления и удаления объектов User .

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

Конечно, другие методы легко реорганизовать, чтобы они могли работать, например, с реляционной базой данных.

Хотя классы User и UserDao сосуществуют независимо в одном приложении, нам все еще нужно посмотреть, как последний можно использовать для сохранения уровня сохраняемости, скрытого от логики приложения:

public class UserApplication { private static Dao userDao; public static void main(String[] args) { userDao = new UserDao(); User user1 = getUser(0); System.out.println(user1); userDao.update(user1, new String[]{"Jake", "[email protected]"}); User user2 = getUser(1); userDao.delete(user2); userDao.save(new User("Julie", "[email protected]")); userDao.getAll().forEach(user -> System.out.println(user.getName())); } private static User getUser(long id) { Optional user = userDao.get(id); return user.orElseGet( () -> new User("non-existing user", "no-email")); } }

Пример надуманный, но он в двух словах показывает мотивы, лежащие в основе паттерна DAO. В этом случае основной метод просто использует экземпляр UserDao для выполнения операций CRUD с несколькими объектами User .

Наиболее важным аспектом этого процесса является то, как UserDao скрывает от приложения все низкоуровневые детали того, как объекты сохраняются, обновляются и удаляются .

3. Использование шаблона с JPA

Среди разработчиков есть общая тенденция думать, что выпуск JPA понижен до нуля функциональности шаблона DAO, поскольку шаблон становится просто еще одним уровнем абстракции и сложности, реализованным поверх уровня, предоставляемого менеджером сущностей JPA.

Несомненно, в некоторых сценариях это правда. Тем не менее , иногда мы просто хотим предоставить нашему приложению только несколько предметно-ориентированных методов API диспетчера сущностей. В таких случаях паттерн DAO имеет свое место.

3.1. JpaUserDao Класс

С учетом сказанного, давайте создадим новую реализацию интерфейса Dao , чтобы мы могли увидеть, как он может инкапсулировать функциональность, предоставляемую менеджером сущностей JPA из коробки:

public class JpaUserDao implements Dao { private EntityManager entityManager; // standard constructors @Override public Optional get(long id) { return Optional.ofNullable(entityManager.find(User.class, id)); } @Override public List getAll() { Query query = entityManager.createQuery("SELECT e FROM User e"); return query.getResultList(); } @Override public void save(User user) { executeInsideTransaction(entityManager -> entityManager.persist(user)); } @Override public void update(User user, String[] params) { user.setName(Objects.requireNonNull(params[0], "Name cannot be null")); user.setEmail(Objects.requireNonNull(params[1], "Email cannot be null")); executeInsideTransaction(entityManager -> entityManager.merge(user)); } @Override public void delete(User user) { executeInsideTransaction(entityManager -> entityManager.remove(user)); } private void executeInsideTransaction(Consumer action) { EntityTransaction tx = entityManager.getTransaction(); try { tx.begin(); action.accept(entityManager); tx.commit(); } catch (RuntimeException e) { tx.rollback(); throw e; } } }

Класс JpaUserDao может работать с любой реляционной базой данных, поддерживаемой реализацией JPA.

Более того, если мы внимательно посмотрим на класс, мы поймем, как использование Composition и Dependency Injection позволяет нам вызывать только те методы диспетчера сущностей, которые требуются нашему приложению.

Проще говоря, у нас есть специализированный API для домена, а не API всего менеджера сущностей.

3.2. Рефакторинг класса пользователя

В этом случае мы будем использовать Hibernate в качестве реализации JPA по умолчанию, поэтому мы соответствующим образом реорганизуем класс User :

@Entity @Table(name = "users") public class User { @Id @GeneratedValue(strategy = GenerationType.AUTO) private long id; private String name; private String email; // standard constructors / setters / getters }

3.3. Программная загрузка JPA Entity Manager

Assuming that we already have a working instance of MySQL running either locally or remotely and a database table “users” populated with some user records, we need to get a JPA entity manager, so we can use the JpaUserDao class for performing CRUD operations in the database.

In most cases, we accomplish this via the typical “persistence.xml” file, which is the standard approach.

In this case, we'll take an “xml-less” approach and get the entity manager with plain Java through Hibernate's handy EntityManagerFactoryBuilderImpl class.

For a detailed explanation on how to bootstrap a JPA implementation with Java, please check this article.

3.4. The UserApplication Class

Finally, let's refactor the initial UserApplication class, so it can work with a JpaUserDao instance and execute CRUD operations on the User entities:

public class UserApplication { private static Dao jpaUserDao; // standard constructors public static void main(String[] args) { User user1 = getUser(1); System.out.println(user1); updateUser(user1, new String[]{"Jake", "[email protected]"}); saveUser(new User("Monica", "[email protected]")); deleteUser(getUser(2)); getAllUsers().forEach(user -> System.out.println(user.getName())); } public static User getUser(long id) { Optional user = jpaUserDao.get(id); return user.orElseGet( () -> new User("non-existing user", "no-email")); } public static List getAllUsers() { return jpaUserDao.getAll(); } public static void updateUser(User user, String[] params) { jpaUserDao.update(user, params); } public static void saveUser(User user) { jpaUserDao.save(user); } public static void deleteUser(User user) { jpaUserDao.delete(user); } }

Even when the example is pretty limited indeed, it remains useful for demonstrating how to integrate the DAO pattern's functionality with the one that the entity manager provides.

In most applications, there's a DI framework, which is responsible for injecting a JpaUserDao instance into the UserApplication class. For simplicity's sake, we've omitted the details of this process.

Наиболее значимая точка , чтобы подчеркнуть здесь, как JpaUserDao класс помогает держать UserApplication класса полностью агностик о том , как операции выполняет CRUD сохранения слоя .

Кроме того, в дальнейшем мы могли бы заменить MySQL на любую другую СУБД (и даже на плоскую базу данных), и, тем не менее, наше приложение продолжало бы работать, как ожидалось, благодаря уровню абстракции, обеспечиваемому интерфейсом Dao и диспетчером сущностей. .

4. Вывод

В этой статье мы подробно рассмотрели ключевые концепции шаблона DAO, как реализовать его на Java и как использовать его поверх диспетчера сущностей JPA.

Как обычно, все примеры кода, показанные в этой статье, доступны на GitHub.