LoginSignup
3
0

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-05-07

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

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

課題

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

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インタフェースを追加します。

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.

該当のIssueでは、Custom Codecsを提供するとのことでしたので、リファレンスをみつつ実装するかーと思ったのですが・・・
2020/5/7時点の最新版であるR2DBC MySQL 0.8.2.RELEASEにおいてはCustom Codecインタフェースが公開されていない不具合が解消されていないようで、まだこの方法は利用できないみたい。

対策

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

まず、こちらを参考にReadingConverterとWritingConverterを実装します。
ReadingConverterでは、取得したRowから該当するカラムのデータを取り出し、エンティティを戻り値としています。
この際に、categoryIdsはJacksonによってList<Int>に変換をしています。

@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では、ArticleOutboundRowに変換しています。
この際、公式のドキュメントではSettableValue.fromメソッドを利用していますが、現在こちらのメソッドは非推奨になっているのでParameter.from等を使いましょう。

@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を登録します。

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

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

Appendix

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

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

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0