魔がさして、RDBMS の JSON 型を導入しようとしてみました*1。
前提条件
データ構造
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 のインスタンスを生成する所に問題があるようです。
これは困った。
どうも、JSON 型を使う時は TEXT 型のように扱うのではダメなようです。
ということは、RDBMS 毎に SQL を変更しないといけないのか...
6. sql ファイルを用意する
Doma 2 では、SQL ファイル名をつけ分けることによって使い分けができそうです。素敵!
insert-h2.sql
insert into customers (
customer_id,
customer_code,
customer_name,
attribute
) values (
'id_001',
'code_001',
'name_002',
'{"hoge"::30}' FORMAT JSON
)
insert-postgres.sql
insert into customers (
customer_id,
customer_code,
customer_name,
attribute
) values (
'id_001',
'code_001',
'name_002',
'{"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)
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 素敵です。