Забудьте о DAO, используйте Repository
Недавно задумался о том, чем отличаются паттерны, позволяющие абстрагироваться от работы с хранилищем данных. Много раз поверхностно читал описания и различные реализации DAO и Repository, даже применял их в своих проектах, видимо, до конца не понимая концептуальных отличий. Решил разобраться, закопался в Google и нашел статью, которая для меня разъяснила все. Подумал, что неплохо было бы перевести ее на русский. Оригинал для англочитающих здесь. Остальным интересующимся добро пожаловать под кат.
Data Access Object (DAO) — широко распространенный паттерн для сохранения объектов бизнес-области в базе данных. В самом широком смысле, DAO — это класс, содержащий CRUD методы для конкретной сущности.
Предположим, что у нас имеется сущность Account, представленная следующим классом:
package com.thinkinginobjects.domainobject; public class Account < private String userName; private String firstName; private String lastName; private String email; private int age; public boolean hasUseName(String desiredUserName) < return this.userName.equals(desiredUserName); >public boolean ageBetween(int minAge, int maxAge) < return age >= minAge && age >
package com.thinkinginobjects.dao; import com.thinkinginobjects.domainobject.Account; public interface AccountDAO
- Отделяет бизнес-логику, использующую данный паттерн, от механизмов сохранения данных и используемых ими API;
- Сигнатуры методов интерфейса независимы от содержимого класса Account. Если вы добавите поле telephoneNumber в класс Account, не будет необходимости во внесении изменений в AccountDAO или использующих его классах.
package com.thinkinginobjects.dao; import java.util.List; import com.thinkinginobjects.domainobject.Account; public interface BloatAccountDAO
- Сложнее создавать моки для интерфейса DAO во время юнит-тестирования. Необходимо было бы реализовывать больше методов DAO даже в тех тестовых сценариях, когда они не используются;
- Интерфейс DAO становится все более привязанным к полям класса Account. Возникает необходимость в изменении интрфейса и его реализаций при изменении типов полей класса Account.
Паттерн Repository
Лучшим решением будет использование паттерна Repository. Эрик Эванс дал точное описание в своей книге: «Repository представляет собой все объекты определенного типа в виде концептуального множества. Его поведение похоже на поведение коллекции, за исключением более развитых возможностей для построения запросов».
Вернемся назад и спроектируем AccountRepository в соответствии с данным определением:
package com.thinkinginobjects.repository; import java.util.List; import com.thinkinginobjects.domainobject.Account; public interface AccountRepository < void addAccount(Account account); void removeAccount(Account account); void updateAccount(Account account); // Think it as replace for set List query(AccountSpecification specification); >
Методы add и update выглядят идентично методам AccountDAO. Метод remove отличается от метода удаления, определенного в DAO тем, что принимает Account в качестве параметра вместо userName (идентификатора аккаунта). Представление репозитория как коллекции меняет его восприятие. Вы избегаете раскрытия типа идентификатора аккаунта репозиторию. Это сделает вашу жизнь легче в том случае, если вы захотите использовать long для идентрификации аккаунтов.
Если вы задумываетесь о контрактах методов add/remove/update, просто подумайте об абстрации коллекции. Если вы задумаетесь о добавлении еще одного метода update для репозитория, подумайте, имеет ли смысл добавлять еще один метод update для коллекции.
Однако, метод query является особенным. Я бы не ожидал увидеть такой метод в классе коллекции. Что он делает?
Репозиторий отличается от коллекции, если рассматривать возможности для построения запросов. Имея коллекцию объектов в памяти, довольно просто перебрать все ее элементы и найти интересующий нас экземпляр. Репозиторий работает с большим набором объектов, чаще всего, находящихся вне оперативной памяти в момент выполнения запроса. Нецелесообразно загружать все аккаунты в память, если нам необходим один конкретный пользователь. Вместо этого, мы передаем репозиторию критерий, с помощью которого он сможет найти один или несколько объектов. Репозиторий может сгенерировать SQL запрос в том случае, если он использует базу данных в качестве бекэнда, или он может найти необходимый объект перебором, если используется коллекция в памяти.
Одна из часто используемых реализаций критерия — паттерн Specification (далее спецификация). Спецификация — это простой предикат, который принимает объект бизнес-области и возвращает boolean:
package com.thinkinginobjects.repository; import com.thinkinginobjects.domainobject.Account; public interface AccountSpecification
Итак, мы можем создавать реализации для каждого способа выполнения запросов к AccountRepository.
Обычная спецификация хорошо работает для репозитория в памяти, но не может быть использована с базой данных из-за неэффективности.
Для AccountRepository, работающего с SQL базой данных, спецификации необходимо реализовать интерфейс SqlSpecification:
package com.thinkinginobjects.repository; public interface SqlSpecification
Репозиторий, использующий базу данных в качестве бекэнда, может использовать данный интерфейс для получения параметров SQL запроса. Если бы в качестве бекэнда для репозитория использовался Hibernate, мы бы использовали интерфейс HibernateSpecification, который генерирует Criteria.
SQL- и Hibernate-репозитории не используется метод specified. Тем не менее, мы находим наличие реализации данного метода во всех классах преимуществом, т.к. таким образом мы сможем использовать заглушку для AccountRepository в тестовых целях а также в кеширующей реализации репозитория перед тем, как запрос будет направлен непосредственно к бекэнду.
Мы даже можем сделать еще один шаг и использовать композицию Spicification с ConjunctionSpecification и DisjunctionSpecification для выполнения более сложных запросов. Нам кажется, что данный вопрос выходит за рамки статьи. Заинтересованный читатель может найти подробности и примеры в книге Эванса.
package com.thinkinginobjects.specification; import org.hibernate.criterion.Criterion; import org.hibernate.criterion.Restrictions; import com.thinkinginobjects.domainobject.Account; import com.thinkinginobjects.repository.AccountSpecification; import com.thinkinginobjects.repository.HibernateSpecification; public class AccountSpecificationByUserName implements AccountSpecification, HibernateSpecification < private String desiredUserName; public AccountSpecificationByUserName(String desiredUserName) < super(); this.desiredUserName = desiredUserName; >@Override public boolean specified(Account account) < return account.hasUseName(desiredUserName); >@Override public Criterion toCriteria() < return Restrictions.eq("userName", desiredUserName); >>
package com.thinkinginobjects.specification; import com.thinkinginobjects.domainobject.Account; import com.thinkinginobjects.repository.AccountSpecification; import com.thinkinginobjects.repository.SqlSpecification; public class AccountSpecificationByAgeRange implements AccountSpecification, SqlSpecification < private int minAge; private int maxAge; public AccountSpecificationByAgeRange(int minAge, int maxAge) < super(); this.minAge = minAge; this.maxAge = maxAge; >@Override public boolean specified(Account account) < return account.ageBetween(minAge, maxAge); >@Override public String toSqlClauses() < return String.format("age between %s and %s", minAge, maxAge); >>
Заключение
Паттерн DAO предоставляет размытое описание контракта. Используя его, выполучаете потенциально неверно используемые и раздутые реализации классов. Паттерн Репозиторий использует метафору коллекции, которая дает нам жесткий контракт и делает понимание вашего кода проще.
Repository
Repository layer is added between the domain and data mapping layers to isolate domain objects from details of the database access code and to minimize scattering and duplication of query code. The Repository pattern is especially useful in systems where number of domain classes is large or heavy querying is utilized.
# Explanation
Let’s say we need a persistent data store for persons. Adding new persons and searching for them according to different criteria must be easy.
Repository architectural pattern creates a uniform layer of data repositories that can be used for CRUD operations.
Repositories are classes or components that encapsulate the logic required to access data sources. They centralize common data access functionality, providing better maintainability and decoupling the infrastructure or technology used to access databases from the domain model layer.
Programmatic Example
Let’s first look at the person entity that we need to persist.
@ToString @EqualsAndHashCode @Setter @Getter @Entity @NoArgsConstructor public class Person @Id @GeneratedValue private Long id; private String name; private String surname; private int age; /** * Constructor. */ public Person(String name, String surname, int age) this.name = name; this.surname = surname; this.age = age; > >
We are using Spring Data to create the PersonRepository so it becomes really simple.
@Repository public interface PersonRepository extends CrudRepositoryPerson, Long>, JpaSpecificationExecutorPerson> Person findByName(String name); >
Additionally we define a helper class PersonSpecifications for specification queries.
public class PersonSpecifications public static class AgeBetweenSpec implements SpecificationPerson> private final int from; private final int to; public AgeBetweenSpec(int from, int to) this.from = from; this.to = to; > @Override public Predicate toPredicate(RootPerson> root, CriteriaQuery?> query, CriteriaBuilder cb) return cb.between(root.get("age"), from, to); > > public static class NameEqualSpec implements SpecificationPerson> public String name; public NameEqualSpec(String name) this.name = name; > public Predicate toPredicate(RootPerson> root, CriteriaQuery?> query, CriteriaBuilder cb) return cb.equal(root.get("name"), this.name); > > >
And here’s the repository example in action.
var peter = new Person("Peter", "Sagan", 17); var nasta = new Person("Nasta", "Kuzminova", 25); var john = new Person("John", "lawrence", 35); var terry = new Person("Terry", "Law", 36); repository.save(peter); repository.save(nasta); repository.save(john); repository.save(terry); LOGGER.info("Count Person records: <>", repository.count()); var persons = (ListPerson>) repository.findAll(); persons.stream().map(Person::toString).forEach(LOGGER::info); nasta.setName("Barbora"); nasta.setSurname("Spotakova"); repository.save(nasta); repository.findById(2L).ifPresent(p -> LOGGER.info("Find by id 2: <>", p)); repository.deleteById(2L); LOGGER.info("Count Person records: <>", repository.count()); repository .findOne(new PersonSpecifications.NameEqualSpec("John")) .ifPresent(p -> LOGGER.info("Find by John is <>", p)); persons = repository.findAll(new PersonSpecifications.AgeBetweenSpec(20, 40)); LOGGER.info("Find Person with age between 20,40: "); persons.stream().map(Person::toString).forEach(LOGGER::info); repository.deleteAll();