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

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


## 単一主キーの場合

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

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

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

```sql
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フィールドを持つだけ

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

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

### エンティティ

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

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

    private String name;
}
```

### リポジトリ

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

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

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

```java
@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();
    }
}
```

### 叩いてみる

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

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

## 複合主キーの場合

### テーブル
codeとnameの複合キーにする

```sql
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');
```

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

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


### エンティティ

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

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

### リポジトリ

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


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

### コントローラ

```java
@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();
    }
}
```


### 叩いてみる

```bash
$ 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()`ってやんないといけない。
- 折衷案として、以下の感じでゲッターを作ってあげれば、使うときにはこの階層構造を気にすること無く使えるのかな、と思っている。

```java
@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();
    }
}
```