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>
<?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-mybatis の IBatisModule で 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)))