0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

いつか使いたいSpring Data JPAの機能

Posted at

最近Spring Data JPAに入門しました。
使用DBはPostgresです。

公式
https://docs.spring.io/spring-data/jpa/reference/index.html

を全部読んだので、今後使えそうな強力な機能をメモとして残しておこうと思います。

MappedSuperclass

全てのEntity共通のパラメーターをまとめるために使えそうです。このアノテーションが付与されたクラスは直接DBとのMappingはされませんが、各Entityがこれを継承したとき、定義されたフィールドがDBにMappingされます。

EntityにCreatedAtやUpdatedAtを追加する際に使おうと思います。

EntityGraph

@Entity
public class Order {
    @Id
    private Long id;

    @OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
    private List<OrderItem> items;
    
    // その他のフィールド・メソッド
}

@Entity
public class OrderItem {
    @Id
    private Long id;

    @ManyToOne
    @JoinColumn(name = "order_id")
    private Order order;
    
    // その他のフィールド・メソッド
}

のようなEntityをクエリする場合、

public interface OrderRepository extends JpaRepository<Order, Long> {
    List<Order> findAll();
}

とすれば一見Order.getItemで子Entityを取れるので、同時に取得できているように見えますが、これはLazyLoadのようです。
もし最終的に全てのOrderItemをなめないといけないことがわかっているような場合

public interface OrderRepository extends JpaRepository<Order, Long> {
    @EntityGraph(attributePaths = {"items"})
    List<Order> findAll();
}

とすることで最初から1つのクエリで子EntityをJOINしてくれます。
N+1の対応として覚えておきます。

Query by Example

例となるEntityを引数にとってEntityの配列を返します。
例えばフロントエンドで氏名などの条件を指定してユーザーを検索できる画面があるとします。その場合JPAで愚直にQueryを宣言していくと、たくさんの種類のQueryを宣言することになってしまいます。
しかしこの機能を使えば、「検索条件のみが指定されたEntity」を例として与えて自動的にクエリしてくれます。

public List<User> searchUsers(String firstname, String lastname, String email, Integer age, String status) {
        User probe = User.builder()
                .firstname(firstname)
                .lastname(lastname)
                .email(email)
                .age(age)
                .status(status)
                .build();
        
        ExampleMatcher matcher = ExampleMatcher.matching()
                .withIgnoreNullValues()
                .withMatcher("firstname", ExampleMatcher.GenericPropertyMatchers.contains().ignoreCase())
                .withMatcher("lastname", ExampleMatcher.GenericPropertyMatchers.contains().ignoreCase())
                .withMatcher("email", ExampleMatcher.GenericPropertyMatchers.exact().ignoreCase())
                .withMatcher("status", ExampleMatcher.GenericPropertyMatchers.exact().ignoreCase());
        
        Example<User> example = Example.of(probe, matcher);
        
        return userRepository.findAll(example);
    }

いろいろな条件が組み合わさる検索画面などを作る場合に使いたいです。

Specification

DDDの「仕様」を引数にクエリを組み立てます。

public class UserSpecifications {
    
    // ユーザー名が一致する条件
    public static Specification<User> hasName(String name) {
        return (root, query, cb) -> 
            cb.equal(root.get("name"), name);
    }

    // 年齢が指定以上である条件
    public static Specification<User> olderThan(int age) {
        return (root, query, cb) ->
            cb.greaterThan(root.get("age"), age);
    }
}

こんな感じの仕様オブジェクトを用意しておいて、

Specification<User> spec = Specification
    .where(hasName("Alice"))
    .and(olderThan(18));
List<User> users = userRepository.findAll(spec);

のように実行できます。
これで「検索条件」という大事なビジネスロジックをRepository層に垂れ流さなくてすみますね。

射影

2つの便利な使い方がありそうです。1つは単純に必要なカラムが少ない場合。インターフェースで必要なカラムを定義して、クエリできます。
具体的には

@Entity
public class User {
    @Id
    private Long id;
    private String firstname;
    private String lastname;
    private String email;
    private String password; // 不要なフィールド
    private String address;  // 不要なフィールド
    // 省略されたゲッター・セッター
}

というEntityから

public interface UserSummary {
    Long getId();
    String getFirstname();
    String getLastname();
    String getEmail();
}

だけを抽出する場合、

public interface UserRepository extends JpaRepository<User, Long> {
    
    // インターフェースベースのプロジェクションを使用
    List<UserSummary> findAllProjectedBy();
    
    // 名前でフィルタリングする場合の例
    List<UserSummary> findByLastname(String lastname);
}

のような感じになります。
一覧表示画面のクエリが遅すぎ問題などに遭遇した場合に積極的に使っていきたいです。(まずはキャッシュ戦略からだと思うけど。)

もうひとつはJOINした結果をDtoクラスとして定義して、そのDtoに直接クエリを発行することができるようです。

public interface UserRepository extends JpaRepository<User, Long> {
    
    @Query("SELECT new com.example.demo.dto.UserProfileDTO(u.id, u.firstname, u.lastname, u.email, p.profilePictureUrl) " +
           "FROM User u LEFT JOIN u.profile p WHERE u.lastname = :lastname")
    List<UserProfileDTO> findUserProfilesByLastname(@Param("lastname") String lastname);
}

それぞれUserとProfileをクエリしてアプリ側でJOIN、という奇行をする前にこの機能のことを思い出したいです。
でもこれを使う前に@EntityGraphとObjectMapperの組み合わせを試してみるべきな気もします。Query宣言が必要なのがちょっと気になる。

集約ルートからのイベント処理

リポジトリにsave(.save())したときに発行されるイベントをEntityに定義できます。
Entityに@DomainEventsをつけたpublicメソッドを実装しておくだけで、saveやsaveAllやupdateで自動でイベントが発行されるようになるみたいです。

@Entity
public class User {

    @Id
    private Long id;
    private String name;
    private String email;

    // コンストラクタ、ゲッター、セッターなど

    public User() {}

    public User(Long id, String name, String email) {
        this.id = id;
        this.name = name;
        this.email = email;
    }

    @DomainEvents
    public Collection<Object> domainEvents() {
        return Collections.singleton(new UserDomainEvent(this));
    }}

メール送信などのイベントをトリガーにした処理を実装するときに使っていきたいです。が、@DomainEventsだとタイミングは制御できないみたいで、saveもupdateもイベントが発行されてしまいそうですから、受け取り側でそれがsaveなのかupdateなのか判断する必要があるかもしれないですね。

以上、Spring Data JPAの公式ドキュメントから便利そうな機能をピックアップして紹介する記事でした。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?