Javaにおけるリポジトリパターン:抽象化された永続化によるデータアクセスを簡素化
リポジトリデザインパターンの目的
リポジトリデザインパターンは、すべての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 - SpecificationsとQuerydsl(Spring)
- リポジトリパターンの利点とSpringでの実装(Stack Overflow)
- 私がよく避けるデザインパターン:リポジトリパターン(InfoWorld)
Javaにおけるリポジトリパターンの実用例
- Spring Data JPAは、JPA実装の上に堅牢なリポジトリ抽象化レイヤーを提供することにより、Java向けに調整されたリポジトリパターンを実例として示しています。
- Hibernate:多くの場合、データエンティティにアクセスして管理するためのリポジトリとして機能するDAOと共に使用されます。
- Java EEアプリケーションでは、ビジネスロジックをデータアクセスコードから分離するために、リポジトリパターンが頻繁に使用されます。
リポジトリパターンの利点とトレードオフ
利点
- データアクセスロジックを集中化することにより、コードの保守性と可読性を向上させます。
- リポジトリのモック実装を許可することにより、テスト性を向上させます。
- ビジネスロジックとデータアクセスレイヤー間の疎結合を促進します。
トレードオフ
- 追加の抽象化レイヤーを導入するため、複雑さが増す可能性があります。
- 抽象化レイヤーによる潜在的なパフォーマンスオーバーヘッド。
関連するJavaデザインパターン
- データマッパー:リポジトリがデータアクセスを処理する一方で、データマッパーはオブジェクトとデータベース間のデータ転送を担当し、データの整合性を維持します。
- ユニットオブワーク:トランザクションの管理とデータへの変更の追跡を行うために、リポジトリと併用されることが多いです。