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, чтобы увидеть, как он может полностью отделить модель предметной области от уровня постоянства.
Вот API DAO:
public interface Dao<T> {
Optional<T> get(long id);
List<T> getAll();
void save(T t);
void update(T t, String[] params);
void delete(T t);
}
С высоты птичьего полета становится ясно, что интерфейс Dao
определяет абстрактный API, который выполняет CRUD-операции над объектами типа T.
Благодаря высокому уровню абстракции, обеспечиваемому интерфейсом, легко создать конкретную, детализированную реализацию, которая работает с объектами User .
2.3. Класс UserDao
_
Давайте определим пользовательскую реализацию интерфейса Dao :
public class UserDao implements Dao<User> {
private List<User> users = new ArrayList<>();
public UserDao() {
users.add(new User("John", "john@domain.com"));
users.add(new User("Susan", "susan@domain.com"));
}
@Override
public Optional<User> get(long id) {
return Optional.ofNullable(users.get((int) id));
}
@Override
public List<User> 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<User> userDao;
public static void main(String[] args) {
userDao = new UserDao();
User user1 = getUser(0);
System.out.println(user1);
userDao.update(user1, new String[]{"Jake", "jake@domain.com"});
User user2 = getUser(1);
userDao.delete(user2);
userDao.save(new User("Julie", "julie@domain.com"));
userDao.getAll().forEach(user -> System.out.println(user.getName()));
}
private static User getUser(long id) {
Optional<User> 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<User> {
private EntityManager entityManager;
// standard constructors
@Override
public Optional<User> get(long id) {
return Optional.ofNullable(entityManager.find(User.class, id));
}
@Override
public List<User> 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<EntityManager> action) {
EntityTransaction tx = entityManager.getTransaction();
try {
tx.begin();
action.accept(entityManager);
tx.commit();
}
catch (RuntimeException e) {
tx.rollback();
throw e;
}
}
}
Класс JpaUserDao
может работать с любой реляционной базой данных, поддерживаемой реализацией JPA.
Кроме того, если мы внимательно посмотрим на класс, мы поймем, как использование композиции и внедрения зависимостей позволяет нам вызывать только те методы менеджера сущностей, которые требуются нашему приложению.
Проще говоря, у нас есть специализированный 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 ****
Предполагая, что у нас уже есть рабочий экземпляр MySQL, работающий локально или удаленно, и таблица базы данных «users»,
заполненная некоторыми пользовательскими записями, нам нужно получить диспетчер сущностей JPA, чтобы мы могли использовать класс JpaUserDao
для выполнения операций CRUD в базе данных. .
В большинстве случаев мы делаем это с помощью стандартного файла persistence.xml
, что является стандартным подходом.
В этом случае мы воспользуемся подходом без XML и получим диспетчер сущностей с простой Java через удобный класс EntityManagerFactoryBuilderImpl Hibernate.
Подробное объяснение того, как запустить реализацию JPA с помощью Java, см . в этой статье .
3.4. Класс UserApplication
_
Наконец, давайте реорганизуем исходный класс UserApplication
, чтобы он мог работать с экземпляром JpaUserDao
и запускать операции CRUD над сущностями пользователя :
public class UserApplication {
private static Dao<User> jpaUserDao;
// standard constructors
public static void main(String[] args) {
User user1 = getUser(1);
System.out.println(user1);
updateUser(user1, new String[]{"Jake", "jake@domain.com"});
saveUser(new User("Monica", "monica@domain.com"));
deleteUser(getUser(2));
getAllUsers().forEach(user -> System.out.println(user.getName()));
}
public static User getUser(long id) {
Optional<User> user = jpaUserDao.get(id);
return user.orElseGet(
() -> new User("non-existing user", "no-email"));
}
public static List<User> 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);
}
}
Пример здесь довольно ограничен. Но это полезно для того, чтобы показать, как интегрировать функциональность шаблона DAO с той, которую предоставляет диспетчер сущностей.
В большинстве приложений есть структура DI, которая отвечает за внедрение экземпляра JpaUserDao в класс
UserApplication
. Для простоты мы опустили детали этого процесса.
Здесь наиболее важным моментом является то, как класс JpaUserDao
помогает классу UserApplication быть
полностью независимым от того, как слой сохраняемости выполняет операции CRUD.
Кроме того, в будущем мы могли бы заменить MySQL на любую другую СУБД (и даже на плоскую базу данных), и наше приложение продолжало бы работать, как и ожидалось, благодаря уровню абстракции, обеспечиваемому интерфейсом Dao
и менеджером сущностей.
4. Вывод
В этой статье мы подробно рассмотрели ключевые концепции шаблона DAO. Мы увидели, как реализовать это на Java и как использовать поверх менеджера сущностей JPA.
Как обычно, все примеры кода, показанные в этой статье, доступны на GitHub .