Java 中的仓储模式:使用抽象持久化简化数据访问
约 4 分钟
仓储设计模式的意图
仓储设计模式充当管理所有 Java 数据访问逻辑的中心枢纽,从应用程序的其余部分抽象出数据存储和检索的细节。
仓储模式的详细解释以及真实世界示例
真实世界示例
想象一个图书馆系统,其中图书管理员充当仓储。与每个图书馆用户通过整个图书馆搜索书籍(数据)不同,他们会找到图书管理员(仓储),图书管理员确切地知道每本书的位置,无论它是在架子上、在储藏室里,还是被其他人借走了。图书管理员抽象了书籍存储的复杂性,允许用户请求书籍,而无需了解存储系统。这种设置简化了用户(客户端)的流程,并将书籍管理(数据访问逻辑)集中起来。
通俗地说
仓储模式为处理所有数据访问逻辑提供了一个中心位置,从应用程序的其余部分抽象出数据存储和检索的复杂性。
Microsoft 文档 表示
仓储是封装访问数据源所需的逻辑的类或组件。它们集中了常见的数据访问功能,从而提高了可维护性,并将用于访问数据库的基础设施或技术与域模型层解耦。
Java 中仓储模式的编程示例
让我们首先看看我们需要持久化的 person 实体。
@ToString
@EqualsAndHashCode
@Setter
@Getter
@Entity
@NoArgsConstructor
public class Person {
@Id
@GeneratedValue
private Long id;
private String name;
private String surname;
private int age;
public Person(String name, String surname, int age) {
this.name = name;
this.surname = surname;
this.age = age;
}
}
我们使用 Spring Data 创建 PersonRepository
,因此它变得非常简单。
@Repository
public interface PersonRepository extends CrudRepository<Person, Long>, JpaSpecificationExecutor<Person> {
Person findByName(String name);
}
此外,我们为规范查询定义了一个辅助类 PersonSpecifications
。
public class PersonSpecifications {
public static class AgeBetweenSpec implements Specification<Person> {
private final int from;
private final int to;
public AgeBetweenSpec(int from, int to) {
this.from = from;
this.to = to;
}
@Override
public Predicate toPredicate(Root<Person> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
return cb.between(root.get("age"), from, to);
}
}
public static class NameEqualSpec implements Specification<Person> {
public String name;
public NameEqualSpec(String name) {
this.name = name;
}
public Predicate toPredicate(Root<Person> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
return cb.equal(root.get("name"), this.name);
}
}
}
以下是在实际操作中仓储示例。
public static void main(String[] args) {
var context = new ClassPathXmlApplicationContext("applicationContext.xml");
var repository = context.getBean(PersonRepository.class);
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);
// Add new Person records
repository.save(peter);
repository.save(nasta);
repository.save(john);
repository.save(terry);
// Count Person records
LOGGER.info("Count Person records: {}", repository.count());
// Print all records
var persons = (List<Person>) repository.findAll();
persons.stream().map(Person::toString).forEach(LOGGER::info);
// Update Person
nasta.setName("Barbora");
nasta.setSurname("Spotakova");
repository.save(nasta);
repository.findById(2L).ifPresent(p -> LOGGER.info("Find by id 2: {}", p));
// Remove record from Person
repository.deleteById(2L);
// count records
LOGGER.info("Count Person records: {}", repository.count());
// find by name
repository
.findOne(new PersonSpecifications.NameEqualSpec("John"))
.ifPresent(p -> LOGGER.info("Find by John is {}", p));
// find by age
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();
context.close();
}
程序输出
INFO [2024-05-27 07:00:32,847] com.iluwatar.repository.App: Count Person records: 4
INFO [2024-05-27 07:00:32,859] com.iluwatar.repository.App: Person(id=1, name=Peter, surname=Sagan, age=17)
INFO [2024-05-27 07:00:32,859] com.iluwatar.repository.App: Person(id=2, name=Nasta, surname=Kuzminova, age=25)
INFO [2024-05-27 07:00:32,859] com.iluwatar.repository.App: Person(id=3, name=John, surname=lawrence, age=35)
INFO [2024-05-27 07:00:32,859] com.iluwatar.repository.App: Person(id=4, name=Terry, surname=Law, age=36)
INFO [2024-05-27 07:00:32,869] com.iluwatar.repository.App: Find by id 2: Person(id=2, name=Barbora, surname=Spotakova, age=25)
INFO [2024-05-27 07:00:32,873] com.iluwatar.repository.App: Count Person records: 3
INFO [2024-05-27 07:00:32,878] com.iluwatar.repository.App: Find by John is Person(id=3, name=John, surname=lawrence, age=35)
INFO [2024-05-27 07:00:32,881] com.iluwatar.repository.App: Find Person with age between 20,40:
INFO [2024-05-27 07:00:32,881] com.iluwatar.repository.App: Person(id=3, name=John, surname=lawrence, age=35)
INFO [2024-05-27 07:00:32,881] com.iluwatar.repository.App: Person(id=4, name=Terry, surname=Law, age=36)
何时在 Java 中使用仓储模式
- 在想要将业务逻辑与 Java 应用程序中的数据访问层解耦时,应用仓储模式,确保代码更灵活、更易于维护。
- 适用于可能使用多个数据源并且业务逻辑应该不知道数据源细节的场景。
- 非常适合测试目的,因为它允许使用模拟仓储。
仓储模式 Java 教程
- 不要使用 DAO,使用仓储(面向对象思考)
- 高级 Spring Data JPA - 规范和 Querydsl (Spring)
- 仓储模式优势和 Spring 实现 (Stack Overflow)
- 我经常避免的设计模式:仓储模式 (InfoWorld)
仓储模式在 Java 中的真实世界应用
- Spring Data JPA 通过为 Java 提供在 JPA 实现之上强大的仓储抽象层,体现了仓储模式。
- Hibernate:通常与充当访问和管理数据实体的仓储的 DAO 一起使用。
- Java EE 应用程序经常使用仓储模式来将业务逻辑与数据访问代码分离。
仓储模式的优势和权衡
优势
- 通过集中数据访问逻辑,提高代码的可维护性和可读性。
- 通过允许模拟仓储实现来增强可测试性。
- 促进业务逻辑与数据访问层之间的松耦合。
权衡
- 引入额外的抽象层,可能会增加复杂性。
- 由于抽象层,可能会出现性能开销。