JPAからR2DBCに切り替え作業

# 更新履歴

| 日付       | 内容 |
|:-----------|:-----|
| 2021.03.09 | 初版 |

# はじめに

ORMを `SpringDataJpa` で実装していたが、WebFluxに足を踏み入れたので、
思い切って `SpringDataR2DBC` にも入門してみた。
とりあえずカスタマイズ/チューニングは無視して、動かせるようにした経過を記録しておく。

- FW
    - Spring Boot 2.4.3
- Libraries
    - spring-boot-starter-webflux
    - spring-boot-starter-data-r2dbc
- JDK
    - AdoptOpenJdk 11.0.6
- Build
    - Gradle 6.8.3
- IDE
    - IntelliJ IDEA Community 2020.3
- Docker
    - mysql:8.0.23

# R2DBC

## セットアップ

1. build.gradleに依存関係を設定
1. application.ymlの修正
1. Javaコード修正

### build.gradle

```groovy:build.gradle
dependencies {
    // JPAをコメントアウトしてR2DBCを追加
    //api("org.springframework.boot:spring-boot-starter-data-jpa")
    api("org.springframework.boot:spring-boot-starter-data-r2dbc")

    // R2DBC用の実装で必要
    runtimeOnly("dev.miku:r2dbc-mysql")
    runtimeOnly("mysql:mysql-connector-java")
}
```

### application.yml

```yml:application.yml
spring: 
  r2dbc:
     url: r2dbcs:mysql://localhost:3306/db
     username: root
     password: root
```

JPAの記述などは全て削除した

## Configクラスの修正

```java:BaseJpaConfig.java
@EntityScan
@EnableJpaRepositories
@EnableTransactionManagement(proxyTargetClass = true)
public abstract class BaseJpaConfig {

}
```

```java:BaseR2dbcConfig.java
@EnableR2dbcRepositories
@EnableTransactionManagement(proxyTargetClass = true)
public abstract class BaseR2dbcConfig extends AbstractR2dbcConfiguration {

}
```

## Dtoの修正

```java:BaseTable.java(修正前)
@Accessors(chain = true)
@Data
@MappedSuperclass
public abstract class BaseTable {

    @CreatedDate
    @Column(name = "created_date_time")
    private LocalDateTime createdDateTime;

    @Column(name = "created_by")
    private Integer createdBy;

    @Version
    @Column(name = "version")
    private Integer version;

    @Column(name = "deleted_flag")
    private Boolean deletedFlag = Boolean.FALSE;

}
```

```java:BaseTable.java(修正後)
@Accessors(chain = true)
@Data
public abstract class BaseTable<ID> implements Persistable<ID> {

  @CreatedDate
  @Column(value = "created_date_time")
  private LocalDateTime createdDateTime;

  @Column(value = "created_by")
  private Integer createdBy;

  @Version
  @Column(value = "version")
  private Integer version;

  @Column(value = "deleted_flag")
  private DeletedFlag deletedFlag = DeletedFlag.is_false;

  @Transient
  private boolean isNew;

  @Getter
  public enum DeletedFlag {
    is_true,
    is_false
  }

}
```

論理削除フラグをTINYINT型で定義していて、JPAでは `Boolean` 型でマッピングしていたが、
`R2DBC` では起動時にエラーがでていた。
なので思い切ってenum型に定義しなおしたところうまく動作した。

## Datasourceの修正

```java:TableSource.java(修正前)
@NoRepositoryBean
public interface TableSource<D extends BaseTable, ID> extends JpaRepository<D, ID>, JpaSpecificationExecutor<D> {

    @Async
    default CompletableFuture<D> insert(D entity, LocalDateTime localDateTime) {
        entity.setCreatedDateTime(localDateTime);
        entity.setVersion(0);
        return CompletableFuture.supplyAsync(() -> save(entity));
    }

    @Async
    default CompletableFuture<D> logicalDelete(D entity, LocalDateTime localDateTime) {
        entity.setDeletedDateTime(localDateTime);
        entity.setDeletedFlag(true);
        return CompletableFuture.supplyAsync(() -> save(entity));
    }

} 
```

```java:TableSource.java(修正後)
@NoRepositoryBean
public interface TableSource<D extends BaseTable, ID> extends ReactiveSortingRepository<D, ID> {

  default Mono<D> insert(D entity, LocalDateTime localDateTime) {
    entity.setCreatedDateTime(localDateTime);
    entity.setVersion(null);
    entity.setNew(true);
    return this.save(entity);
  }

  default Mono<D> logicalDelete(D entity, LocalDateTime localDateTime) {
    entity.setDeletedDateTime(localDateTime);
    entity.setDeletedFlag(DeletedFlag.is_true);
    entity.setNew(false);
    return this.save(entity);
  }

}
```

## サンプルソース

### 1件取得

```java:StoreRepositoryImpl.java
public Mono<StoreEntity> findBy(StoreId storeId) {
    return this.findDto(storeId)
        .map(this::convertStoreEntity);
}

private Mono<Store> findDto(StoreId storeId) {
    return this.source.findById(storeId.intValue())
        .filter(BaseTable::isExists)
        // 404で返却するためのエラーを検討
        .switchIfEmpty(Mono.error(new RuntimeException("店舗が存在しません")));
}
```

### 1件更新

```java:StoreRepositoryImpl.java
public Mono<StoreId> modify(StoreEntity entity, LocalDateTime receptionTime) {
    return this.findDto(entity.getStoreId())
        .map(dto -> this.attach(dto, entity))
        .flatMap(dto -> this.source.update(dto, receptionTime))
        .map(Store::getId)
        .map(StoreId::new);
  }

private Mono<Store> findDto(StoreId storeId) {
    return this.source.findById(storeId.intValue())
        .filter(BaseTable::isExists)
        // 404で返却するためのエラーを検討
        .switchIfEmpty(Mono.error(new RuntimeException("店舗が存在しません")));
}

private Store attach(Store dto, StoreEntity entity) {
    var storeId = Optional.ofNullable(entity.getStoreId())
        .map(StoreId::intValue)
        .orElse(null);
    var store = Optional.of(dto).orElse(new Store());
    return store
        .setId(storeId)
        .setName(entity.getName());
}
```

Monoをchainする場合、`flatMap` で繋ぐ

# つまづいた箇所

`ReactiveCrudRepository` にentity1件検索のfindOneがない😅
絞り込み検索が `Mono.filter()` 以外あるかわからない😅

# 今後の取り組み
`CompletableFuture` から `Mono` , `Flux` を動かせるように変更しただけなので、APIのレスポンスが遅くなってしまった。
チューニングしていきたい。。。