【Java / Kotlin】Entityでよく使うカラムを共通化する

## はじめに

Entityでよく使うカラムの共通化の仕方について、
考えたことをまとめておきます。

## Javaで共通化

各テーブルに共通して存在するカラム（たとえば `created_at`とか）は抽象クラスに切り出し。

- 抽象クラス

```java:CommonEntity.java
/**
 * 共通Entity（各テーブルで保持する項目）
 */
@MappedSuperclass
@Data
public abstract class CommonEntity {

  @Column(updatable = false)
  private String createdBy;
  @Column(updatable = false)
  private LocalDateTime createdAt;
  private String updatedBy;
  private LocalDateTime updatedAt;

  // 登録前に共通的に実行されるメソッド（呼び出し不要）
  @PrePersist
  public void preInsert() {
    LocalDateTime localDateTime = LocalDateTime.now();
    setCreatedAt(localDateTime);
    setUpdatedAt(localDateTime);
  }

  // 更新前に共通的に実行されるメソッド（呼び出し不要）
  @PreUpdate
  public void preUpdate() {
    setUpdatedAt(LocalDateTime.now());
  }

  // 登録前に共通的に実行されるメソッド
  public void initUserId(CustomRequestHeaderForm header) {
    this.setCreatedBy(header.getLoginUserId());
    this.setUpdatedBy(header.getLoginUserId());
  }

  // 更新前に共通的に実行されるメソッド
  public void updateUserId(CustomRequestHeaderForm header) {
    this.setUpdatedBy(header.getLoginUserId());
  }
}
```

- エンティティ

```java:MemoEntity.java
@Entity
@Getter
@Setter
@Builder
@Table(name = "memo")
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class MemoEntity extends CommonEntity implements Serializable {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String title;

  private String content;

  @Builder.Default
  private Boolean isDeleted = ApplicationConstant.IS_NOT_DELETED;

  public void delete() {
    isDeleted = ApplicationConstant.IS_DELETED;
  }
}
```

### なにがうれしいか

- 登録前や更新前に実行したいお決まりの処理を切り出すことができる
    - エンティティを定義するたびに、同じことを書く必要がなくなる
- エンティティがスッキリする
- ドメインの実装に集中できる

## Kotlinで共通化

上記と同じことを実装しようとすると、Kotlinでは以下のようになりました。

- 抽象クラス

ちなみに、`data class`はスーパークラスになれないみたいです。

```kotlin:CommonEntity.kt
@MappedSuperclass
abstract class CommonEntity {
    @Column(updatable = false)
    lateinit var createdAt: LocalDateTime
    lateinit var updatedAt: LocalDateTime

    @Column(updatable = false)
    lateinit var createdBy: String
    lateinit var updatedBy: String

    fun initUserId(header: CustomRequestHeaderForm) {
        createdBy = header.loginUserId
        updatedBy = header.loginUserId
    }

    fun updateUserId(header: CustomRequestHeaderForm) {
        updatedBy = header.loginUserId
    }

    @PrePersist
    fun preInsert() {
        val now = LocalDateTime.now()
        createdAt = now
        updatedAt = now
    }

    @PreUpdate
    fun preUpdate() {
        updatedAt = LocalDateTime.now()
    }
}
```

- エンティティ

```kotlin:MemoEntity.kt
@Entity
@Table(name = "memo")
data class MemoEntity(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id: Long?,
    val title: String,
    val content: String,
    var isDeleted: Boolean,
): CommonEntity() {
  fun delete() {
    isDeleted = ApplicationConstant.IS_DELETED
  }
}

```

### 微妙だなと思ったこと
#### コンストラクタでまとめて初期化できない

抽象クラスのフィールドはコンストラクタ内で初期化できないのでスッキリしません。。
（Javaも同じではありますが、Kotlinではコンストラクタで完結できるとスッキリ書けるので・・）

- NG

本当はこんな感じに書きたい。けどエラる。

```kotlin:MemoRequestForm.kt
data class MemoRequestForm (
    val title: String,
    val content: String,
) {
    fun toEntity(header: CustomRequestHeaderForm, id: Long? = null) =
        MemoEntity(
            id,
            title,
            content,
            header.loginUserId // ←抽象クラスのフィールドなのでエラる！！
        )
    }
}
```

- OK
以下だとエラーにならない。
```kotlin:MemoRequestForm.kt
data class MemoRequestForm (
    val title: String,
    val content: String,
) {
    fun toEntity(header: CustomRequestHeaderForm, id: Long? = null): MemoEntity {
      val entity = MemoEntity(
          id,
          title,
          content,
      )
      entity.createdBy = header.loginUserId // 別で代入してあげる必要がある。
      return entity
    }
  }
}
```

#### 抽象クラスで`var`を使ってしまっている

上記の通り、コンストラクタ内ですべてのフィールドを初期化できないがために、
以下のように抽象クラスを使わなければ全て`val`で書けるのに、`var`で書く必要が出てしまいます。

- good
抽象クラスで切り出さない場合は全て`val`で書ける。
```kotlin:MemoEntity.kt
@Entity
@Table(name = "memo")
data class MemoEntity(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id: Long?,
    val title: String,
    val content: String,
    val isDeleted: Boolean,
    @Column(updatable = false)
    val createdAt: LocalDateTime,
    val updatedAt: LocalDateTime,
    @Column(updatable = false)
    val createdBy: String,
    val updatedBy: String,
)
```

## まとめ

以上、JavaとKotlinでのEntityの共通カラムの切り出し方についてまとめてみました。
Kotlinで抽象クラスを使うのは、もしかすると微妙かもしれません・・（好みの問題）

私はどっちがよいのか２、３日考えましたが、答えは出せず。。

皆さんのJava / Kotlinでの共通化Tips、あればぜひ教えてください！！