java-Mybatis カスタム JsonObjectTypeHandler はデータベースの JSON 型データを動的に解析します
環境
- jdk 1.8
- スプリングブーツ 1.5.6
- PostgreSQL 14.5
- ハウスボート 3.53
- postgresql 42.2.1
導入
主流のデータベースは Json データ型をサポートしていますが、Mybatis はそれを十分にサポートしていないため、処理用に独自の TypeHandler を作成する必要があります。最近、pg ライブラリを使用しているときに、json 型データのクエリと分析の問題に遭遇し、非常に刺激を受けて多くの情報を確認しましたが、ニーズを満たす関連情報が見つかりませんでした。そこで私はコードを追跡し、Mybaits PGProvider のデフォルト データ型変換部分のコード ロジックを分析し、関連する一般的なクエリの JSON 解析問題を解決しました。
発生する問題は、サービス クエリのテーブル名とフィールド名が不確実であるため、事前に多数のエンティティ モデルを生成できないことです。SQL は、動的クエリのテーブル名 + フィールド名を通じてのみ動的に生成できます。クエリ結果を汎用にするために、Mybatis クエリ結果を受け入れるために List<Map<String,Object>> 型を使用します。 List はデータの行を表し、Map はフィールドと値の対応関係であり、最終的にサービス メッセージが Json 型で返されます。
ハンドラーの役割は?
デフォルトのクエリを使用してJson型データを変換すると、以下の形式で出力されます。
{
"type":"json",
"value":"{\"\aa":\"11\",\"bb\":\"22\"}"
}
この形式には対応するハンドラーが必要です。このハンドラーを見つけて置き換えることで問題が解決します。
ハンドラーの取得ロジックを見つけます。
ステップバイステップで最後まで進めていくと、基礎となるいくつかの変換メソッドを慎重に分析する必要があることがわかりました。
//解析妹一行的结果值
org.apache.ibatis.executor.resultset.DefaultResultSetHandler#getRowValue(org.apache.ibatis.executor.resultset.ResultSetWrapper, org.apache.ibatis.mapping.ResultMap, java.lang.String)
//创建字段和java类型的映射关系
org.apache.ibatis.executor.resultset.DefaultResultSetHandler#createAutomaticMappings()
//获取jdbcType类型使用的handler对象
org.apache.ibatis.executor.resultset.ResultSetWrapper#getTypeHandler(Class<?> propertyType, String columnName)
getTypeHandler() メソッド
//其中的一条命令是使用java类型+jdbc类型在typeHandlerRegistry中查找handler
handler = typeHandlerRegistry.getTypeHandler(propertyType, jdbcType);
//getTypeHandler的实现不多贴出来瞧瞧
//org.apache.ibatis.type.TypeHandlerRegistry#getTypeHandler(java.lang.reflect.Type, org.apache.ibatis.type.JdbcType)
private <T> TypeHandler<T> getTypeHandler(Type type, JdbcType jdbcType) {
if (ParamMap.class.equals(type)) {
return null;
}
Map<JdbcType, TypeHandler<?>> jdbcHandlerMap = getJdbcHandlerMap(type); //通过JDBC类型获取到了三个实现
TypeHandler<?> handler = null;
if (jdbcHandlerMap != null) {
handler = jdbcHandlerMap.get(jdbcType);
if (handler == null) {
handler = jdbcHandlerMap.get(null);
}
if (handler == null) {
// #591
handler = pickSoleHandler(jdbcHandlerMap);
}
}
// type drives generics here
return (TypeHandler<T>) handler;
}
//上面方法中的jdbcHandlerMap内容为:
{null=class java.lang.Object, OTHER=class java.lang.Object, ARRAY=class java.lang.Object}
//由于没有对应的handler,所以最总new了一个默认的handler:handler = new ObjectTypeHandler();
//org.apache.ibatis.type.ObjectTypeHandler
//以上为handler的查找定位,接下来的代码是比较有意思的,有关数据库中的数据是如何转为java对象的。(题外话了)
//org.postgresql.jdbc.PgResultSet#internalGetObject
//org.postgresql.jdbc.PgResultSet#getString(int) --获取一个字符
public String getString(int columnIndex) throws SQLException {
...
Encoding encoding = connection.getEncoding();
try {
/*
1.this_row表示一行的数据,使用byte[][]类型的二维数据表示,第一维是字段索引,第二维是字段值,知道jdbcType就可以将不同类型转为Java类型了
2.byte的取值范围是[-128, 127],是不是就限制了每个pg的每个表最多只能有0~127个索引共128个字段呢???
*/
return trimString(columnIndex, encoding.decode(this_row[columnIndex - 1]));
} catch (IOException ioe) {
...
}
}
上記の位置付けを通じて、すべてのハンドラーは、構成の TypehandlerRegisty オブジェクトの typeHandlerMap プライベート プロパティから間接的に取得されることがわかります。
typehandlerMap の代入メソッドを見ると、これにコピーされている呼び出しが register(...) メソッドであることがわかりました。数えてみると、実際には 12 個のオーバーロードがありました -_-!!! ただし、このプライベートのメソッドは 1 つだけ
です顔を変える割り当て、それは次のとおりです。
private void register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler) {
if (javaType != null) {
Map<JdbcType, TypeHandler<?>> map = typeHandlerMap.get(javaType);
if (map == null || map == NULL_TYPE_HANDLER_MAP) {
map = new HashMap<>();
typeHandlerMap.put(javaType, map);
}
map.put(jdbcType, handler);
}
allTypeHandlersMap.put(handler.getClass(), handler);
}
ハンドラー オブジェクトの取得プロセスを追跡すると、次の 3 つの重要なパラメーターがわかります。
- db 内の json は jdbcType.OTHER で表されます。
- db 内の json は java.Object で表されます。
- これは実装する必要があるカスタム ハンドラーであり、BaseTypeHandler<> 抽象クラスから継承されます。
カスタムハンドラーの登録
登録に使用する方法は、@autowired を通じて登録されているすべての SqlSessionFactory を取得し、SqlSessionFactory を走査して各インスタンスのハンドラーを登録することです。
@Configuration
public class MybatisConfig {
@Autowired
private List<SqlSessionFactory> sqlSessionFactoryList;
@PostConstruct
public void addSqlInterceptor() {
SchemaParamsterInterceptor interceptor = new SchemaParamsterInterceptor();
for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
...
//注册json对象处理器
//registryJsonObjectTypeHandler(sqlSessionFactory);
}
}
private void registryJsonObjectTypeHandler(SqlSessionFactory sqlSessionFactory ){
org.apache.ibatis.session.Configuration config = sqlSessionFactory.getConfiguration();
//以下两个注册方法针对的私有对象不同,一个是jdbcTypeHandlerMap另一个是typeHandlerMap
config.getTypeHandlerRegistry().register(JdbcType.OTHER, new JsonObjectTypeHandler());
config.getTypeHandlerRegistry().register(Object.class,JdbcType.OTHER, new JsonObjectTypeHandler());
}
}
JsonObjectTypeHandlerの実装
カスタム Typehandler が実装されるため、BaseTypeHandler 基本クラスを継承するのが自然です。ここで、T はジェネリック型であり、T を決定するプロセスについては以下で説明します。
これは com.alibaba.fastjson.JSONObject です
json 文字列を返すには、com.alibaba.fastjson が再度使用されます。最初のアイデアは、JSONObject 型を使用することです。これは
データベースに {"a": "1", "b": "2"} として保存されます。クエリ 次は非常に完璧で通常の分析です。
しかし、データベースが配列に格納されている場合はどうなるでしょうか? 例: [{"a": "1", "b": "2"},…] または ["a", "b",…] の場合、カスタム変換は失敗します。
fastjson には特別な処理配列があり、そのオブジェクトは com.alibaba.fastjson.JSONArray です。
Tはcom.alibaba.fastjson.JSONArray
このとき、配列型の処理は問題ありませんが、[]が削除されます。また論理が異常だ!!!!
{"a": "1", "b": "2"}, [{"a": "1", "b": "2"},...] これら 2 種類の json はどのように共存するのでしょうか?
JSONObject と JSONArray は包含関係であり、JSONObjet に [] を追加するだけで JSONArray 型に変更できます。
この問題は解決できますが、データベースに保存されているデータとは大きく異なるため、この解決策はお勧めできません。
T は java.lang.Object
なぜ実装クラスにこだわるのでしょうか? JSONObject と JSONArray にも共通点があり、最上位層は Object オブジェクトを継承する必要があります。
また、json 文字列配列とオブジェクトの違いも比較的簡単に区別でき、中国語展開 ([) で始まる文字列であれば配列です。そこで、データであるかどうかを判断して、
fastjsonの2つの異なる変換メソッドを使用して変換し、変換されたオブジェクトインスタンスをObject経由で受け取ることで、配列とオブジェクトの問題を完全に解決します。
要約する
カスタム TypeHandler を構成して使用する方が簡単で、基本クラスを継承していくつかのメソッドを実装するだけで問題ありません。しかし、そこには常に推測の要素が含まれており、それがどのように機能し、どのように実装されているかがわかりません。
カスタム ハンドラーが成功したかどうか、また、その位置に登録されているかどうかさえわかりません。発効する必要があります。コード分析プロセスを分析しなければ、「ブラックボックス」の外でしかコードを使用できません。
問題に遭遇し、問題を解決することは、いくつかのプロセスを学ぶ良い方法です。時間と進歩が許せば、「角」に取り掛かることができます。