Spring Data R2DBCでMySQLのJSONを取り扱う

Spring Data R2DBC + r2dbc-mysqlでJSONを取り扱うときにハマったので備忘録として書き残しておきます。
結論としては、Spring Data R2DBCのCustom Conversionsを利用しました。

なお、本記事ではコードを原則Kotlinで記述しています。

## 課題
下記のようなSQLで作成されるテーブルをMySQL上に持つとします。

```sql
create table article
(
    id           bigint unsigned auto_increment,
    category_ids json not null,
    constraint article_pk primary key (id)
);

--- data exmaple
INSERT INTO article
VALUES (null, json_array(1, 2));
```

このテーブルは「記事」エンティティに関するテーブルで、カテゴリIDのリストを保持したカラムを持ちます。

そして、このテーブルに対応する「記事」エンティティを次のように定義し、ReactiveCrudRipositoryインタフェースを追加します。

```kotlin:Article.kt
data class Article(
    @Id
    val Id: Long? = null,
    val categoryIds: List<Int>,
)
```

```ArticleRepository.kt
interface ArticleRepository : ReactiveCrudRepository<Article, Long>
```

これで、`articleRepository.findById(1)`のようにしたら問題なくエンティティを取得できそうな気もしますが、下記のようなエラーが発生します。

```
Failed to convert from type [java.lang.String] to type [java.lang.Integer] for value ‘[1]’; nested exception is java.lang.NumberFormatException: For input string: “[1]”
```


## 原因
色々調べてみたところ、r2dbc-mysqlでは現在、JSONタイプをサポートせず、VARCHARとしてパススルーさせているようです。
ドライバ「側」が複数のライブラリ等に対するサポートを行うのは望ましくないからという考えからそのような実装になっているようですね。

> JSON mapping is typically an application-level concern (JSON-P, JSON-B) because all transformations and bindings are reflected on application or client-level. Having support directly in the driver comes with several drawbacks as drivers need to either commit to a single library or provide support for multiple implementations (Jackso, GSON, Apache Johnzon). This isn't typically the thing you want to deal with.

https://github.com/mirromutth/r2dbc-mysql/issues/15

該当のIssueでは、Custom Codecsを提供するとのことでしたので、[リファレンス](https://github.com/mirromutth/r2dbc-mysql#add-a-codec)をみつつ実装するかーと思ったのですが・・・
2020/5/7時点の最新版である[R2DBC MySQL 0.8.2.RELEASE](https://github.com/mirromutth/r2dbc-mysql/releases/tag/v0.8.2.RELEASE)においてはCustom Codecインタフェースが公開されていない不具合が解消されていないようで、まだこの方法は利用できないみたい。

https://github.com/mirromutth/r2dbc-mysql/issues/154

## 対策
結局、冒頭で述べた通りですがSpring Data R2DBCのCustom Conversionsという機能を利用することにしました。


まず、[こちら](https://github.com/spring-projects/spring-data-r2dbc/blob/main/src/main/asciidoc/reference/mapping.adoc#overriding-mapping-with-explicit-converters)を参考にReadingConverterとWritingConverterを実装します。
ReadingConverterでは、取得したRowから該当するカラムのデータを取り出し、エンティティを戻り値としています。
この際に、`categoryIds`はJacksonによって`List<Int>`に変換をしています。

```kotlin
@ReadingConverter
class ArticleReadConverter : Converter<Row, Article> {
    override fun convert(source: Row): Article {
        val id = source.get("id", Long::class.java)
        val categoryIdsString = source.get("category_ids", String::class.java)
        val mapper = ObjectMapper()
        val categoryIds = mapper.readValue(categoryIdsString, object : TypeReference<List<Int>>() {})
        return Article(id = id, code = code!!, name = name!!, categoryIds = categoryIds)
    }
}
```

WritingConverterでは、`Article`を`OutboundRow`に変換しています。
この際、公式のドキュメントでは`SettableValue.from`メソッドを利用していますが、現在こちらのメソッドは非推奨になっているので`Parameter.from`等を使いましょう。

```kotlin
@WritingConverter
class ArticleWriteConverter : Converter<Article, OutboundRow> {
    override fun convert(source: Article): OutboundRow {
        val row = OutboundRow()
        with(row) {
            put("id", Parameter.fromOrEmpty(source.id, Long::class.java))
            put("category_ids", Parameter.from(ObjectMapper().writeValueAsString(source.categoryIds)))
        }
        return row
    }
}
```

最後に、`AbstractR2dbcConfiguration`クラスを継承した設定クラスを実装し、`getCustomConverters`メソッドで実装したReadingConverterとWritingConverterを登録します。

```kotlin
@Configuration
class R2dbcConfiguration(
    private val connectionFactory: ConnectionFactory
) : AbstractR2dbcConfiguration() {
    override fun connectionFactory() = connectionFactory
    override fun getCustomConverters() = mutableListOf<Any>(ArticleReadConverter(), ArticleWriteConverter())
}
```

これで、「記事」エンティティを取得したり永続化したりできるようになりました！

### Appendix
- [spring-data-r2dbc - Mapping](https://github.com/spring-projects/spring-data-r2dbc/blob/main/src/main/asciidoc/reference/mapping.adoc)
- [Spring Data R2DBC - Reference Documentation #17.3. Mapping Configuration](https://docs.spring.io/spring-data/r2dbc/docs/1.0.x/reference/html/#mapping.configuration)


下は、StackOverflowで質問して、結局自己解決したのでセルフ回答したやつ。

https://stackoverflow.com/questions/67417483/spring-data-r2dbc-custom-conversions-does-not-work-with-json-array

最後までお読みいただきありがとうございました。