MySQL JSON型をmybatis で読み書きするサンプル

MySQL バージョン 5.7.8 以降で使える JSON型に対する mybatis のハンドラ実装のサンプルです。
JSON型のデータが、単純な key-value だけでなくネストしたオブジェクトが入ったJSONでも、
Java のクラスのネストと対応できることを示すためのサンプルです。

https://github.com/yipuran/yipuran-mybatis
を使います。この yipuran-mybatis のバージョン4.7 以降から、
jackson-databind バージョン 2.12.4 以上を指定した依存関係を持つようになっており Jackson の
JSON変換を利用できます。
サンプルのMySQL テーブル構造

DELIMITER //
DROP TABLE IF EXISTS sakila.t_item
//
CREATE TABLE sakila.t_item(
  id                    INT  NOT NULL AUTO_INCREMENT
, name                  VARCHAR(40) NOT  NULL
, message               JSON NOT NULL
, PRIMARY KEY (id)
 ) ENGINE=InnoDB  DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci
//
DELIMITER ;

サンプルデータ格納

TRUNCATE TABLE t_item;
INSERT INTO t_item( name, message ) VALUES
('オレンジ', '{ "price": 200, "limit": "2021-12-07" }')
,('レモン', JSON_OBJECT("price", 180, "limit", "2021-12-08"))
,('メロン', '{ "price": 1200, "limit": "2021-12-16" , "other":{ "label":"ABC", "width": 230 } }')
;
commit;

SELECT * FROM t_item
;

↑↑↑↑↑
メロンのレコード、message は、JSON_OBJECT関数を使わずに、
ネストしたJSON を '{ }' の中に、"other":{ } で、作成することができる。

テーブル t_item のエンティティクラス
JSON データを格納するクラス sample.entity.Messageを含む

import lombok.Data;
import sample.entity.Message;
/**
 * Titem
 */
@Data
public class Titem{
   private int id;
   private String name;
   private Message message;
}

JSONとしてテーブルに格納されている Message クラス

package sample.entity;
import java.time.LocalDate;
import lombok.Data;
/**
 * Message
 */
@Data
public class Message{
   private int price;
   private LocalDate limit;
   private MessageNest1 other;
}

Message のネスト other のクラス

import lombok.Data;
/**
 * MessageNest1
 */
@Data
public class MessageNest1{
   public String label;
   public int width;
}

JSONタイプハンドラを準備
yipuran-mybatis の AbstractJsonTypeHandler を継承して用意する。
sample.types.JsonTypeHandler.java

package sample.types;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import org.yipuran.mybatis.types.AbstractJsonTypeHandler;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
/**
 * JsonTypeHandler
 */
public class JsonTypeHandler<T> extends AbstractJsonTypeHandler<T> {
   public JsonTypeHandler(Class<T> clazz){
      super(clazz);
   }
   @Override
   public SimpleModule getModule(){
      JavaTimeModule jtm = new JavaTimeModule();
      jtm.addSerializer(new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")))
     .addSerializer(new LocalDateSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")))
      .addSerializer(LocalTimeSerializer.INSTANCE)
      .addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")))
     .addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")))
      .addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ISO_LOCAL_TIME));
      return jtm;
   }
}

.addSerializer(LocalDateSerializer.INSTANCE) を使ってしまうと、[yyyy, MM, dd] シリアライズになってしまうので、
.addSerializer(new LocalDateSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")))
を使った方が良い

mybatis のDB接続設定とSQLMap宣言
上記、JsonTypeHandler を、JSON型の Message に指定する。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
   <settings>
      <setting name="mapUnderscoreToCamelCase" value="true"/>
   </settings>
   <typeHandlers>
     <typeHandler handler="org.labo.dbtest.types.JsonTypeHandler" javaType="sample.entity.Message"/>
   </typeHandlers>
   <environments default="deployment">
      <environment id="deployment">
         <transactionManager type="JDBC" />
         <dataSource type="POOLED">
         <property name="driver" value="com.mysql.cj.jdbc.Driver" />
            <property name="url" value="jdbc:mysql://localhost:3306/testDB" />
            <property name="username" value="root" />
            <property name="password" value="admin123" />
            <property name="defaultAutoCommit" value="false" />
            <property name="poolPingQuery" value="SELECT 1 FROM DUAL" />
            <property name="poolPingEnabled" value="true" />
            <property name="poolMaximumActiveConnections" value="10"/>
            <property name="poolMaximumIdleConnections"   value="10" />
            <property name="poolTimeToWait" value="60000" />
         </dataSource>
      </environment>
   </environments>
   <mappers>
      <mapper resource="sqlmap.xml"/>
   </mappers>
</configuration>

sqlmap.xmlSQL Mapper を定義、

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="sample.mapper.SimpleMapper">

<update id="updateTitem" parameterType="org.labo.dbtest.entity.Titem">
UPDATE t_item SET message = #{message} WHERE id = #{id}
</update>

</mapper>

アノテーションで SELET と INSERT の SQL
INSERT のマッピングでは、message列 にタイプハンドラ JsonTypeHandlerを指定する

package sample.mapper.SimpleMapper;
import java.util.List;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Select;
import org.labo.dbtest.entity.Titem;

public interface SimpleMapper{
   @Select("SELECT * FROM t_item ")
   public List<Titem> getTItems();

   @Insert("INSERT INTO t_item(name, message)VALUES( #{name},#{message, typeHandler=sample.types.JsonTypeHandler} )")
   public void insertM(Titem item);
}

↑ INSEERT で、typeHandlerを指定した書き方であるが、タイプハンドラをコンフィグで全体に指定するならここで
書かなくても良い
Java のロジック
SampleLogic,java

import java.util.List;
public interface SampleLogic{
   public List<Titem> getTItems();
   public void insertM(Titem item);
   public void updateTitem(Titem item);
}

SampleLogic の実装

import java.util.List;
import org.labo.dbtest.entity.Titem;
import org.labo.dbtest.mapper.SimpleMapper;
import org.yipuran.mybatis.IBatisDao;
import org.yipuran.mybatis.QueryLimited;
import org.yipuran.mybatis.Transaction;
public class SampleLogicImpl extends IBatisDao implements SampleLogic{
   @QueryLimited
   @Override
   public List<Titem> getTItems(){
      return getMapper(SimpleMapper.class).getTItems();
   }
   @Transaction
   @Override
   public void insertM(Titem item){
      getMapper(SimpleMapper.class).insertM(item);
   }
   @Transaction
   @Override
   public void updateTitem(Titem item){
      getSqlSession().update(bindId(SimpleMapper.class, "updateTitem"), item);
   }
}

参照クエリの実行例
yipuran-mybatisIBatisModule で Configuration.xml で記述したSQLMapの sqlmap.xml を読ませる。
Google guice でインジェクトした SampleLogic を実行して
MySQL のテーブル t_item を SELECT した結果を標準出力する。

import java.util.List;
import org.yipuran.mybatis.IBatisModule;
import com.google.inject.Guice;
import com.google.inject.Injector;
Injector injector = Guice.createInjector(new IBatisModule());
SampleLogic logic = injector.getInstance(SampleLogicImpl.class);
List<Titem> list = logic.getTItems();
list.stream().forEach(e->{
   System.out.println(e.getId()+" "+e.getName()+ "  "+e.getMessage() );
});

SELECT 標準出力結果

1 オレンジ  Message(price=200, limit=2021-12-07, other=null)
2 レモン  Message(price=180, limit=2021-12-08, other=null)
3 メロン  Message(price=1200, limit=2021-12-16, other=MessageNest1(label=ABC, width=230))

更新の例、

Injector injector = Guice.createInjector(new IBatisModule());
SampleLogic logic = injector.getInstance(SampleLogicImpl.class);
List<Titem> list = logic.getTItems();
list.stream().filter(e->e.getId()==3).findAny().ifPresent(e->{
   Titem t = e;
   System.out.println(t);
   Message m = t.getMessage();
   MessageNest1 n = m.getOther();
   n.setLabel("testA");
   m.setOther(n);
   t.setMessage(m);
   // 更新実行
   logic.updateTitem(t);
});
System.out.println("\"other\":{\"label\":\"testA\"\"testA\"  ");
list.stream().filter(e->e.getId()==3).findAny().ifPresent(e->{
   System.out.println(e);
});

更新実行の標準出力結果

Titem(id=3, name=メロン, message=Message(price=1200, limit=2021-12-16, other=MessageNest1(label=ABC, width=230)))
"other":{"label":"testA" ⇒ "testA"  
Titem(id=3, name=メロン, message=Message(price=1200, limit=2021-12-16, other=MessageNest1(label=testA, width=230)))