1. 概要

Spring DataのCrudRespository#save は間違いなく単純ですが、1つの機能が欠点になる可能性があります。それは、テーブルのすべての列を更新することです。 これがCRUDのUのセマンティクスですが、代わりにPATCHを実行したい場合はどうなりますか?

このチュートリアルでは、完全な更新ではなく部分的な更新を実行するための手法とアプローチについて説明します。

2. 問題

前述のように、 save()は、一致したエンティティを提供されたデータで上書きします。つまり、部分的なデータを提供することはできません。 これは、特にフィールドが多い大きなオブジェクトの場合、不便になる可能性があります。

ORMを見ると、いくつかのパッチが存在します。

  • Hibernateの@DynamicUpdateアノテーションは、更新クエリを動的に書き換えます
  • updatable パラメーターを使用して特定の列の更新を禁止できるため、JPAの@Columnアノテーション

ただし、この問題には特定の目的でアプローチします。私たちの目的は、ORMに依存せずにsaveメソッド用にエンティティを準備することです。

3. 私たちのケース

まず、構築しましょう お客様 実在物:

@Entity 
public class Customer {
    @Id 
    @GeneratedValue(strategy = GenerationType.AUTO)
    public long id;
    public String name;
    public String phone;
}

次に、単純なCRUDリポジトリを定義します。

@Repository 
public interface CustomerRepository extends CrudRepository<Customer, Long> {
    Customer findById(long id);
}

最後に、CustomerServiceを準備します。

@Service 
public class CustomerService {
    @Autowired 
    CustomerRepository repo;

    public void addCustomer(String name) {
        Customer c = new Customer();
        c.name = name;
        repo.save(c);
    }	
}

4. ロードおよび保存アプローチ

まず、おそらくおなじみのアプローチを見てみましょう。データベースからエンティティをロードしてから、必要なフィールドのみを更新します。 これは、私たちが使用できる最も単純なアプローチです。

お客様の連絡先データを更新するメソッドをサービスに追加しましょう。

public void updateCustomerContacts(long id, String phone) {
    Customer myCustomer = repo.findById(id);
    myCustomer.phone = phone;
    repo.save(myCustomer);
}

findById メソッドを呼び出して、一致するエンティティを取得します。 次に、必要なフィールドを更新して更新し、データを永続化します。

この基本的な手法は、更新するフィールドの数が比較的少なく、エンティティがかなり単純な場合に効率的です。

更新する数十のフィールドで何が起こりますか?

4.1. マッピング戦略

オブジェクトに異なるアクセスレベルのフィールドが多数ある場合、DTOパターンを実装することは非常に一般的です。

ここで、オブジェクトに100を超えるphoneフィールドがあるとします。 以前に行ったように、DTOからエンティティにデータを注ぐメソッドを作成することは、煩わしく、かなり保守不可能な場合があります。

それでも、マッピング戦略を使用して、特に MapStruct 実装を使用して、この問題を解決できます。

CustomerDtoを作成しましょう。

public class CustomerDto {
    private long id;
    public String name;
    public String phone;
    //...
    private String phone99;
}

また、CustomerMapperも作成します。

@Mapper(componentModel = "spring")
public interface CustomerMapper {
    void updateCustomerFromDto(CustomerDto dto, @MappingTarget Customer entity);
}

@MappingTarget アノテーションを使用すると、既存のオブジェクトを更新できるため、多くのコードを記述する手間が省けます。

MapStructには@BeanMappingメソッドデコレータがあり、マッピングプロセス中にnull値をスキップするルールを定義できます。

updateCustomerFromDtoメソッドインターフェイスに追加しましょう。

@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)

これにより、JPA save メソッドを呼び出す前に、保存されたエンティティをロードしてDTOとマージできます。実際、更新されるのは変更された値のみです。

それでは、マッパーを呼び出すメソッドをサービスに追加しましょう。

public void updateCustomer(CustomerDto dto) {
    Customer myCustomer = repo.findById(dto.id);
    mapper.updateCustomerFromDto(dto, myCustomer);
    repo.save(myCustomer);
}

このアプローチの欠点は、更新中にnull値をデータベースに渡すことができないことです。

4.2. より単純なエンティティ

最後に、アプリケーションの設計段階からこの問題に取り組むことができることを忘れないでください。

エンティティをできるだけ小さく定義することが重要です。

Customerエンティティを見てみましょう。

少し構造化して、すべてのphoneフィールドをContactPhoneエンティティに抽出し、1対多の関係にします。

@Entity public class CustomerStructured {
    @Id 
    @GeneratedValue(strategy = GenerationType.AUTO)
    public Long id;
    public String name;
    @OneToMany(fetch = FetchType.EAGER, targetEntity=ContactPhone.class, mappedBy="customerId")    
    private List<ContactPhone> contactPhones;
}

コードはクリーンであり、さらに重要なことに、私たちは何かを達成しました。 これで、すべての phone データを取得して入力しなくても、エンティティを更新できます。

小さく制限されたエンティティを処理すると、必要なフィールドのみを更新できます。

このアプローチの唯一の不便な点は、過剰設計の罠に陥ることなく、意識を持ってエンティティを設計する必要があることです。

5. カスタムクエリ

実装できるもう1つのアプローチは、部分的な更新のカスタムクエリを定義することです。

実際、JPAは@Modifying@Queryの2つのアノテーションを定義しており、更新ステートメントを明示的に記述できます。

これで、ORMに負担をかけることなく、更新中の動作方法をアプリケーションに指示できます。

カスタム更新メソッドをリポジトリに追加しましょう。

@Modifying
@Query("update Customer u set u.phone = :phone where u.id = :id")
void updatePhone(@Param(value = "id") long id, @Param(value = "phone") String phone);

これで、更新メソッドを書き直すことができます。

public void updateCustomerContacts(long id, String phone) {
    repo.updatePhone(id, phone);
}

これで、部分的な更新を実行できます。 ほんの数行のコードで、エンティティを変更することなく、目標を達成しました。

この手法の欠点は、オブジェクトの部分的な更新の可能性ごとにメソッドを定義する必要があることです。

6. 結論

部分的なデータ更新は非常に基本的な操作です。 ORMで処理することはできますが、完全に制御できると有益な場合があります。

これまで見てきたように、データをプリロードしてから更新したり、カスタムステートメントを定義したりできますが、これらのアプローチが意味する欠点とその克服方法に注意してください。

いつものように、この記事のソースコードはGitHubから入手できます。