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 че операциите изпълнява боклук на обекти от тип T .
Поради високото ниво на абстракция, което интерфейсът осигурява, е лесно да се създаде конкретна, фина реализация, която да работи с потребителски обекти.
2.3. В UserDao Class
Нека дефинираме специфична за потребителя реализация на интерфейса 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 и 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 Class
С това казано, нека създадем нова реализация на интерфейса 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 and Dependency Injection ни позволява да извикваме само методите на мениджъра на обекти, изисквани от нашето приложение.
Просто казано, ние имаме специфичен за домейн API, а не API на целия мениджър на обекти.
3.2. Рефакториране на потребителския клас
В този случай ще използваме Hibernate като изпълнение на JPA по подразбиране, като по този начин ще рефакторираме съответно потребителския клас:
@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 класа напълно агностик за това как операции постоянство слой изпълнява боклук .
В допълнение, бихме могли да заменим MySQL за всяка друга RDBMS (и дори за плоска база данни) по-надолу и въпреки това нашето приложение ще продължи да работи според очакванията, благодарение на нивото на абстракция, осигурено от интерфейса Dao и мениджъра на обекти .
4. Заключение
В тази статия разгледахме задълбочено ключовите концепции на DAO модела, как да го внедрим в Java и как да го използваме върху мениджъра на обекти на JPA.
Както обикновено, всички примерни кодове, показани в тази статия, са достъпни в GitHub.