魔がさして、RDBMS の JSON 型を導入しようとしてみました*1。
前提条件
- Doma 2
- 2.34.1-SNAPSHOT
- jackson
- 2.11.0
- 検証した RDBMS
- H2
- PostgreSQL
データ構造
create table customers ( customer_id varchar(255) not null constraint customers_pkey primary key, customer_code varchar(255) not null, customer_name varchar(255) not null, attribute json );
エンティティクラス、ドメインクラス
Doma での エンティティクラスは以下のような感じです。
@Entity @Table(name = "customers") @Data public class Customer { @Id @Column(name = "customer_id") private String customerId; @Column(name = "customer_code") private String customerCode; @Column(name = "customer_name") private String customerName; /** 永続化時に JSON カラムにするプロパティ. */ @Column(name = "attribute") private Attribute attribute; }
Attribute class は Doma の ドメインクラス相当です。こいつをまるっと JSON 型に入れたり取り出したりしちゃおうというのが今回のゴールです。
@Data public class Attribute { @JsonProperty("memo") private String memo; @JsonProperty("age") private Integer age; @JsonProperty("point") private Long point; }
手順
1. DomainConverter 作成
Attribute に対する DomainConverter を作成します。 型パラメータの String は JSON 文字列になります。
@ExternalDomain public class AttributeConverter implements DomainConverter<Attribute, String> { private static final ObjectMapper MAPPER = new ObjectMapper(); @Override public String fromDomainToValue(Attribute attribute) { try { return MAPPER.writeValueAsString(attribute); } catch (JsonProcessingException e) { throw new RuntimeException(e); } } @Override public Attribute fromValueToDomain(String value) { try { return value == null ? null : MAPPER.readValue(value, Attribute.class); } catch (JsonProcessingException e) { throw new RuntimeException(e); } } }
2. DomainConverters に追加
DomainConverter を @DomainConverters
に登録します。
今回のサンプルでは DomainConverterProvider クラスで管理しています。
この辺 で Doma に教えてます。
3. Dao インターフェース作成
最初の CustomerDao はこんな感じでした。
@Dao public interface CustomerDao { @Select @Sql("SELECT * FROM customers WHERE customer_id = /*customerId*/'customer_001'") Optional<Customer> selectByCustomerId(String customerId); @Insert int insert(Customer customer); }
4. 実行
テストクラスから実行します。
PostgreSQL で実行した所、
org.seasar.doma.jdbc.SqlExecutionException: [DOMA2009] The SQL execution is failed. PATH=[null]. SQL=[insert into customers (customer_id, customer_code, customer_name, attribute) values ('b9d9900c-27da-4d88-b9a6-028b1a1d32ae', 'CUSTOMER_001', 'NAME_001', '{"memo":"memo_001","age":30,"point":343}')]. The cause is as follows: org.postgresql.util.PSQLException: ERROR: column "attribute" is of type json but expression is of type character varying ヒント: You will need to rewrite or cast the expression. 位置: 98 The root cause is as follows: org.postgresql.util.PSQLException: ERROR: column "attribute" is of type json but expression is of type character varying ヒント: You will need to rewrite or cast the expression. 位置: 98
どうも INSERT 時の JSON 型の扱いがよくないようです。
今度は H2 で実行してみましょう。
java.lang.RuntimeException: com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `examples.domain.Attribute` (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('{"memo":"memo_001","age":30,"point":343}') at [Source: (String)""{\"memo\":\"memo_001\",\"age\":30,\"point\":343}""; line: 1, column: 1]
SELECT の結果(JSON 文字列)を受け取って Java のインスタンスを生成する所に問題があるようです。 これは困った。
5. RDBMS で JSON 型を使う時
どうも、JSON 型を使う時は TEXT 型のように扱うのではダメなようです。
ということは、RDBMS 毎に SQL を変更しないといけないのか...
6. sql ファイルを用意する
Doma 2 では、SQL ファイル名をつけ分けることによって使い分けができそうです。素敵!
insert-h2.sql
-- H2 用の JSON カラムを含む INSERT insert into customers ( customer_id, customer_code, customer_name, attribute ) values ( /* customer.customerId */'id_001', /* customer.customerCode */'code_001', /* customer.customerName */'name_002', /* customer.attribute */'{"hoge"::30}' FORMAT JSON )
insert-postgres.sql
-- PostgreSQL 用の JSON カラムを含む INSERT insert into customers ( customer_id, customer_code, customer_name, attribute ) values ( /* customer.customerId */'id_001', /* customer.customerCode */'code_001', /* customer.customerName */'name_002', /* customer.attribute */'{"hoge"::30}'::json )
CustomerDao#insert を SQL ファイルを使用するように設定しなおします。
@Dao public interface CustomerDao { @Select @Sql("SELECT * FROM customers WHERE customer_id = /*customerId*/'customer_001'") Optional<Customer> selectByCustomerId(String customerId); @Insert(sqlFile = true) // sqlFile=true 追加 int insert(Customer customer); }
7. 再度実行
テストクラスから実行します。 どちらも通りました。やりましたね。
まとめ
PostgreSQL だと JSON 型より JSONB 型を使いそうですが、JSON 文字列として JSON 型にデータを入れる、JSON 型からデータを取り出すことができました。
実際に JSON 関数を使った生 SQL 発行するパターンは未検証ですが、RDBMS 毎にちがうでしょうし、運用時に SQL 叩くことを想定したら TEXT 型よりも JSON 型の方が楽だろう、というモチベーションだったのでアプリケーションからは JSON 関数を意識した SELECT 文は発行しないとは思うので、必要があったら検証することにします。
H2 と PostgreSQL 両方に対応しようとしてたのは、
- Repository の Unit テスト / Integration テストは H2 を使って早く CI を回す
- 本番は PostgreSQL
の構成でやりたかったからです。ただ、INSERT の時点で RDBMS を意識しないといけないのはちょっとアレなので、Unit テストも PostgreSQL で CI した方が良いのかなーと思い始めました*2。
コード
ここにおきます。ここから fork しました。Doma2 素敵です。
*1:アンチパターンだというのはごもっともなのですが、それはそれで
*2:テストクラスが増えてって、CircleCI の parallelism 増やした時に、その分 PostgreSQL コンテナが立ち上がると思うのでリソース足りるのかなーというのが心配