Шаблон 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, щоб ми могли побачити, як він може утримувати модель домену повністю відокремленою від рівня стійкості.

Ось DAO API:

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 .

Завдяки високому рівню абстракції, який забезпечує інтерфейс, легко створити конкретну, дрібну реалізацію, яка працює з об’єктами користувача .

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 і 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-операцій над кількома об'єктами користувача .

Найважливішою стороною цього процесу є те, як 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.

Крім того, якщо ми уважно розглянемо клас, ми зрозуміємо, як використання інжекції композиції та залежності дозволяє нам викликати лише методи керування сутностями, які вимагає наша програма.

Простіше кажучи, ми маємо спеціальний 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.