spring
spring-boot
spring-data-jpa

Spring Data JPAで主キーに値オブジェクトを使う方法(複合キーバージョンも)

概要

Spring Data JPAで主キーに値オブジェクトを使う方法のメモ。
複合キーにする場合のやり方も。

単一主キーの場合

単一主キーの場合はとても簡単にできる。
シンプルなAPIを作りながら手順を示す。

事前準備

Spring Initializerでwebとlombokにチェックを入れてプロジェクト作成。

テーブル

以下のスキーマでテーブルを作る。
カラムはcodeとnameを持ち、codeを主キーとする。

CREATE TABLE customers (
  code VARCHAR(4) NOT NULL,
  name VARCHAR(255) NOT NULL,
  PRIMARY KEY(code)
);

INSERT INTO customers VALUES('1111', 'tanaka');
INSERT INTO customers VALUES('2222', 'suzuki');

値オブジェクト

今回はシンプルにcodeフィールドを持つだけ

@Data
public class Code implements Serializable {
    private String code;
}

@Dataはlombokのアノテーションです。

エンティティ

フィールドに上で作った値オブジェクトを指定する。
@Embeddedアノテーションをつけるのがポイント。
主キーとして扱うので@Idアノテーションもつける。

@Entity
@Data
@Table(name = "customers")
public class Customer {
    @Embedded
    @Id
    private Code code;

    private String name;
}

リポジトリ

引数に値オブジェクトを渡すようにする。

public interface CustomerRepository extends JpaRepository<Customer, Code> {
    public List<Customer> findByCode(Code code);
}

コントローラ

適当に値オブジェクト作って、リポジトリ叩いてデータ返すだけ

@RestController
@RequestMapping("/customer")
@RequiredArgsConstructor
public class CustomerController {

    private final CustomerRepository repository;

    @GetMapping
    public String getCustomer() {
        Code code = new Code();
        code.setCode("1111");
        return repository.findByCode(code).toString();
    }
}

叩いてみる

$ curl -X GET "http://localhost:8090/customer"
[Customer(code=Code(code=1111), name=tanaka)]

解説

  • エンティティのフィールドに値オブジェクトを指定する際は@Embeddedをつける
  • リポジトリの extends JpaRepository<Customer, Code>のところ、ジェネリクスの2個目の部分は値オブジェクトの型にする

複合主キーの場合

テーブル

codeとnameの複合キーにする

CREATE TABLE customers (
  code VARCHAR(4) NOT NULL,
  name VARCHAR(255) NOT NULL,
  PRIMARY KEY(code,name) // nameを追加
);

INSERT INTO customers VALUES('1111', 'tanaka');
INSERT INTO customers VALUES('2222', 'suzuki');

複合主キー用のクラスを作る

@Embeddable
@Data
public class CustomerId implements Serializable {
    @Embedded
    private Code code;
    private String name;
}

エンティティ

上で作った複合主キー用のクラスをフィールドに宣言して、@EmbeddedIdアノテーションをつける。
引数なしのコンストラクタを作らないと怒られる。

@Entity
@Data
@Table(name = "customers")
@NoArgsConstructor
public class Customer {
    @EmbeddedId
    private CustomerId customerId;
}

リポジトリ

メソッド名が変わる。
findBy複合主キークラス名_複合主キークラスのフィールド名になる。

public interface CustomerRepository extends JpaRepository<Customer, CustomerId> {
    public List<Customer> findByCustomerId_Code(Code code);
}

コントローラ

@RestController
@RequestMapping("/customer")
@RequiredArgsConstructor
public class CustomerController {

    private final CustomerRepository repository;

    @GetMapping
    public String getCustomer() {
        Code code = new Code();
        return repository.findByCustomerId_Code(code).toString();
    }
}

叩いてみる

$ curl -X GET "http://localhost:8080/customer"
Customer(customerId=CustomerId(code=Code(code=1111), name=tanaka))

解説

  • 複合キー用のクラスCustomerIdを作って、エンティティでCustomerId型のフィールドを宣言する
  • リポジトリのメソッド名はfindByCustomerId_Code(Code code)とかfindByCustomerId_name(String name)って感じにする

所感

  • 階層的なデータ構造がちょっと気になる。実装の都合でデータの構造が崩れてる感。この場合Entityからcodeを取得するには、customer.getCustomerId().getCode()ってやんないといけない。
  • 折衷案として、以下の感じでゲッターを作ってあげれば、使うときにはこの階層構造を気にすること無く使えるのかな、と思っている。
@Entity
@Data
@Table(name = "customers")
@NoArgsConstructor
public class Customer {
    @EmbeddedId
    private CustomerId customerId;

    // これを追加
    public Code getCode() {
        return customerId.getCode();
    }

    // これを追加
    public String getName() {
        return customerId.getName();
    }
}