Mybatis操作mysql 8的Json欄位型別
Json欄位是從mysql 5.7起加進來的全新的欄位型別,現在我們看看在什麼情況下使用該欄位型別,以及用mybatis如何操作該欄位型別
一般來說,在不知道欄位的具體數量的時候,使用該欄位是非常合適的,比如說——商品的無限屬性。
現在我們來假設這麼一個場景,在商品的二級分類中給商品定義足夠多的屬性,我們先設計屬性的類
/** * 商品自定義屬性 */ @NoArgsConstructor @AllArgsConstructor public class OtherProperty implements Serializable { @Getter @Setterprivate Long id; //屬性id @Getter @Setter private FormType formType; //前端使用的表單型別 @Getter @Setter private String name; //屬性名稱 @Getter @Setter private String unit; //單位 @Getter @Setter private String values; //可選值以@分隔,如配件@車品 @Getter privateList<String> valueList = new ArrayList<>(); //對可選值的取值列表 @Getter @Setter private String defaultValue; //可選值中的預設值 @Getter @Setter private boolean search; //是否可搜尋 @Getter @Setter private boolean mustWrite; //是否必錄 @Getter @Setter private Boolean used= false; //是否已經在商品中使用,已使用該屬性則不允許修改 public OtherProperty changeValuesToList() { String[] split = this.values.split("@"); for (String value : split) { this.valueList.add(value); } this.values = null; return this; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; OtherProperty that = (OtherProperty) o; if (!id.equals(that.id)) return false; if (search != that.search) return false; if (mustWrite != that.mustWrite) return false; if (formType != that.formType) return false; if (!name.equals(that.name)) return false; if (unit != null ? !unit.equals(that.unit) : that.unit != null) return false; if (values != null ? !values.equals(that.values) : that.values != null) return false; return defaultValue != null ? defaultValue.equals(that.defaultValue) : that.defaultValue == null; } @Override public int hashCode() { int result = id.hashCode() + formType.hashCode() + name.hashCode(); result = result + (unit != null ? unit.hashCode() : 0); result = result + (values != null ? values.hashCode() : 0); result = result + (defaultValue != null ? defaultValue.hashCode() : 0); result = result + (search ? 1 : 0); result = result + (mustWrite ? 1 : 0); return result; } }
其中formType為列舉型別
public enum FormType implements Localisable { TYPE1("文字框"), TYPE2("下拉框"), TYPE3("單選框"), TYPE4("複選框"), TYPE5("多行文字框"); private String value; private FormType(String value) { this.value = value; } @Override public String getValue() { return this.value; } }
我們來看一下商品分類的部分程式碼
@AllArgsConstructor @NoArgsConstructor public class ProviderProductLevel implements Provider,Serializable
其中包含一個商品屬性物件的列表
@Getter @Setter private List<OtherProperty> otherProperties;
部分操作原始碼如下
/** * 通過二級配件分類id查詢其包含的所有其他屬性 * @param * @return */ public List<OtherProperty> findOtherProperties() { if (this.level == 2) { LevelDao levelDao = SpringBootUtil.getBean(LevelDao.class); String ids = levelDao.findIdsByLevel2Id(this.id); return levelDao.findOtherProperties(ids); } return null; } /** * 在二級配件分類中刪除其他屬性的id * @param paramIds * @return */ public boolean deletePropertyIdfromLevel(String paramIds) { if (this.level == 2) { LevelDao levelDao = SpringBootUtil.getBean(LevelDao.class); String ids = levelDao.findIdsByLevel2Id(this.id); String[] idsArray = ids.split(","); List<String> idsList = Arrays.asList(idsArray); List<String> contentIdsList = new ArrayList<>(); contentIdsList.addAll(idsList); String[] paramArray = paramIds.split(","); List<String> paramList = Arrays.asList(paramArray); if (contentIdsList.containsAll(paramList)) { contentIdsList.removeAll(paramList); } if (contentIdsList.size() > 0) { StringBuilder builder = new StringBuilder(); contentIdsList.stream().forEach(eachId -> builder.append(eachId + ",")); String newIds = builder.toString().substring(0, builder.toString().length() - 1); levelDao.addOtherPropertiesToLevel(new ParamOtherPropertiesId(newIds, this.id)); }else { levelDao.addOtherPropertiesToLevel(new ParamOtherPropertiesId("",this.id)); } return true; } return false; }
/** * 展示某二級配件分類的所有其他屬性 * @param id * @return */ @SuppressWarnings("unchecked") @Transactional @GetMapping("/productprovider-anon/showproperties") public Result<List<OtherProperty>> showOtherProperties(@RequestParam("id") Long id) { Provider level2 = levelDao.findLevel2(id); return Result.success(((ProviderProductLevel)level2).findOtherProperties()); } /** * 修改某二級配件分類的其他屬性 * @param id * @param otherProperties * @return */ @SuppressWarnings("unchecked") @Transactional @PostMapping("/productprovider-anon/changeother") public Result<String> changeOtherProperties(@RequestParam("id") Long id,@RequestBody List<OtherProperty> otherProperties) { //獲取配件二級分類物件 Provider level2 = levelDao.findLevel2(id); //獲取未使用的配件二級分類的其他屬性(沒有任何商品使用過該屬性) List<OtherProperty> unUsedList = Optional.ofNullable(((ProviderProductLevel) level2).getOtherProperties()).map(otherProperties1 -> otherProperties1.stream()) .orElse(new ArrayList<OtherProperty>().stream()) .filter(otherProperty -> !otherProperty.getUsed()) .collect(Collectors.toList()); //獲取已使用的配件二級分類的其他屬性 List<Long> usedIdList = Optional.ofNullable(((ProviderProductLevel) level2).getOtherProperties()).map(otherProperties1 -> otherProperties1.stream()) .orElse(new ArrayList<OtherProperty>().stream()) .filter(otherProperty -> otherProperty.getUsed()) .map(OtherProperty::getId) .collect(Collectors.toList()); //在傳遞回來的配件二級分類其他屬性中校對沒有修改過的,沒有使用過的其他屬性,只對修改過的,沒有使用過的其他屬性進行 //儲存,否則不處理 List<OtherProperty> changeList = otherProperties.stream().filter(otherProperty -> Optional.ofNullable(otherProperty.getId()).isPresent()) .filter(otherProperty -> !unUsedList.contains(otherProperty)) .filter(otherProperty -> !usedIdList.contains(otherProperty.getId())) .peek(otherProperty -> otherPropertyDao.deleteOtherPropertiesById(otherProperty.getId())) .collect(Collectors.toList()); if (changeList.size() > 0) { StringBuilder builder = new StringBuilder(); changeList.stream().map(OtherProperty::getId).forEach(eachId -> builder.append(eachId + ",")); String newIds = builder.toString().substring(0, builder.toString().length() - 1); ((ProviderProductLevel) level2).deletePropertyIdfromLevel(newIds); ((ProviderProductLevel) level2).addOtherProperties(changeList); } //獲取新增的其他屬性進行追加到配件二級分類的其他屬性中 List<OtherProperty> newList = otherProperties.stream().filter(otherProperty -> !Optional.ofNullable(otherProperty.getId()).isPresent()) .peek(otherProperty -> otherProperty.setId(idService.genId())) .collect(Collectors.toList()); ((ProviderProductLevel) level2).addOtherProperties(newList); return Result.success("修改成功"); }
在進行一番增刪改查後,資料庫中的資料大致如下
我們查高階項鍊的所有屬性的結果如下
現在我們要在屬於該商品分類中新增商品,商品類定義大致如下
@Data @NoArgsConstructor public class ProviderProduct implements Provider { private Product product; //配件元資訊物件 private String code; //配件編碼 private Brand brand; //品牌 private String details; //配件圖文說明 private String levelName; //二級配件分類名稱 private DefaultProvider provider; //配件商 private ExtBeanWrapper otherValues; //其他屬性集合 }
其中對應於屬性列表的欄位為otherValues,這個值正是我們要存入資料庫的Json欄位型別對映。
商品的資料庫表結構如下
要使用mybatis的資料對Json欄位型別的轉換,可以先引用一個網上寫好的轉換器,當然也可以自己寫
pom
<dependency> <groupId>com.github.jeffreyning</groupId> <artifactId>extcol</artifactId> <version>0.0.1-RELEASE</version> </dependency>
配置檔案中新增 type-handlers-package: com.nh.micro.ext.th
mybatis: type-aliases-package: com.cloud.model.productprovider type-handlers-package: com.nh.micro.ext.th mapper-locations: classpath:/mybatis-mappers/* configuration: mapUnderscoreToCamelCase: true
在mapper檔案中寫入一段插入語句
<insert id="saveProduct" parameterType="com.cloud.productprovider.composite.ProviderProduct"> insert into product (id,name,code,model,normal_price,price_begin,product_imgs,details,brand_id,other_property_value) values (#{product.id},#{product.name},#{code},#{product.model},#{product.price.normalPrice}, <choose> <when test="product.price.begin"> 1 </when> <otherwise> 0 </otherwise> </choose>, #{product.productImgs}, #{details}, #{brand.id}, #{otherValues,jdbcType=VARCHAR} ) </insert>
對應商品分類的每一個自定義屬性,我們可以先拿到該自定義屬性的id,然後以該id,取值為鍵值對進行插入
{
"product":{
"name":"AAAA",
"model":"AAAAA",
"price":{
"normalPrice":199,
"begin":false
},
"productImgs":"http://123.433.567.988"
},
"code":"0001",
"details":"<html><body><a href='sfasffg'><img url='sdfsgwer' /></a></body></html>",
"brand":{
"id":1,
"name":"飛利浦"
},
"otherValues":{
"innerMap":{
"2459623566996408120":"10",
"2459623566996409144":"呼和浩特",
"2459623566996410168":"飛利浦",
"2459623566996411192":"國際",
"2459623566996412216":"包郵"
}
}
}
執行之後,資料庫的資料如下
該外掛的資料類和轉換器的原始碼如下,其實也是很簡單的
public class ExtBeanWrapper { public ExtBeanWrapper() { }; public ExtBeanWrapper(Object entity) { this.setObj(entity); } private Map innerMap = new HashMap(); public Map getInnerMap() { return innerMap; } public void setInnerMap(Map innerMap) { this.innerMap = innerMap; } public void setObj(Object entity) { if (entity == null) { innerMap = null; } JSON jobj = (JSON) JSON.toJSON(entity); innerMap = JSON.toJavaObject(jobj, Map.class); } public Object getObj() { if (innerMap == null) { return null; } JSON jobj = (JSON) JSON.toJSON(innerMap); Map entity = JSON.toJavaObject(jobj, Map.class); return entity; } public Object getObj(Class targetClass) { if (innerMap == null) { return null; } JSON jobj = (JSON) JSON.toJSON(innerMap); Object entity = JSON.toJavaObject(jobj, targetClass); return entity; } }
MappedTypes(com.nh.micro.ext.ExtBeanWrapper.class) @MappedJdbcTypes(JdbcType.VARCHAR) public class TagToJsonTypeHandler extends BaseTypeHandler<ExtBeanWrapper> { private Map jsonToMap(String value) { if (value == null || "".equals(value)) { return Collections.emptyMap(); } else { return JSON.parseObject(value, new TypeReference<Map<String, Object>>() { }); } } @Override public void setNonNullParameter(PreparedStatement ps, int i, ExtBeanWrapper parameter, JdbcType jdbcType) throws SQLException { ps.setString(i, JSON.toJSONString(parameter.getInnerMap())); } public boolean isJson(String value){ if(value==null || "".equals(value)){ return false; }else{ if(value.startsWith("{")){ return true; } } return false; } @Override public ExtBeanWrapper getNullableResult(ResultSet rs, String columnName) throws SQLException { String value=rs.getString(columnName); Map innerMap=jsonToMap(value); ExtBeanWrapper extBeanTag=new ExtBeanWrapper(); extBeanTag.setInnerMap(innerMap); return extBeanTag; } @Override public ExtBeanWrapper getNullableResult(ResultSet rs, int columnIndex) throws SQLException { String value=rs.getString(columnIndex); Map innerMap=jsonToMap(value); ExtBeanWrapper extBeanTag=new ExtBeanWrapper(); extBeanTag.setInnerMap(innerMap); return extBeanTag; } @Override public ExtBeanWrapper getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { String value=cs.getString(columnIndex); Map innerMap=jsonToMap(value); ExtBeanWrapper extBeanTag=new ExtBeanWrapper(); extBeanTag.setInnerMap(innerMap); return extBeanTag; } }
現在我們來看一下如何將該Json欄位從資料庫取出,還是以上面的案例為例,先在mapper檔案中定義一組resultMap
<resultMap id="productMap" type="com.cloud.productprovider.composite.ProviderProduct"> <id property="code" column="code" /> <result property="details" column="details" /> <association property="product" javaType="com.cloud.model.productprovider.Product" column="id"> <id property="id" column="id" /> <result property="name" column="name" /> <result property="model" column="model" /> <result property="productImgs" column="product_imgs" /> <association property="price" javaType="com.cloud.model.serviceprovider.Price"> <id property="normalPrice" column="normal_price" /> <result property="secKillPrice" column="seckill_price" /> <result property="begin" column="price_begin" typeHandler="com.cloud.productprovider.untils.BoolIntTypeHandler" /> </association> </association> <association property="brand" javaType="com.cloud.model.productprovider.Brand" column="brand_id" select="findBrandById" /> <association property="levelName" column="level_id" javaType="java.lang.String" select="findLevelNameById" /> <association property="provider" column="default_provider_id" javaType="com.cloud.productprovider.composite.DefaultProvider" select="findProviderById" /> <association property="otherValues" javaType="com.nh.micro.ext.ExtBeanWrapper" column="other_property_value"> <id property="entry" column="other_property_value" jdbcType="VARCHAR" typeHandler="com.nh.micro.ext.th.TagToJsonTypeHandler" /> </association> </resultMap>
這裡稍微解釋一下,price裡的begin是boolean型別,price_begin在資料庫中是整形,有一個轉換器,程式碼如下
public class BoolIntTypeHandler extends BaseTypeHandler<Boolean> { @Override public void setNonNullParameter(PreparedStatement ps, int i, Boolean parameter, JdbcType jdbcType) throws SQLException { ps.setBoolean(i,parameter); } @Override public Boolean getNullableResult(ResultSet rs, String columnName) throws SQLException { int value = rs.getInt(columnName); if (rs.wasNull()) { return false; }else { if (value == 0) { return false; }else if (value == 1) { return true; } } return false; } @Override public Boolean getNullableResult(ResultSet rs, int columnIndex) throws SQLException { int value = rs.getInt(columnIndex); if (rs.wasNull()) { return false; }else { if (value == 0) { return false; }else if (value == 1) { return true; } } return false; } @Override public Boolean getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { int value = cs.getInt(columnIndex); if (cs.wasNull()) { return false; }else { if (value == 0) { return false; }else if (value == 1) { return true; } } return false; } }
品牌這裡有一個查詢
<select id="findBrandById" parameterType="java.lang.Long" resultType="com.cloud.model.productprovider.Brand"> select id,code,name,sort,log_url logoUrl from brand <where> id=#{brand_id} </where> </select>
配件二級分類名稱
<select id="findLevelNameById" parameterType="java.lang.Long" resultType="java.lang.String"> select name from product_level <where> id=#{level_id} </where> </select>
配件商資訊
<resultMap id="defaultProviderMap" type="com.cloud.productprovider.composite.DefaultProvider"> <id property="code" column="code" /> <association property="productProvider" javaType="com.cloud.model.productprovider.ProductProvider"> <id property="id" column="id" /> <result property="name" column="name" /> </association> </resultMap>
<select id="findProviderById" parameterType="java.lang.Long" resultMap="defaultProviderMap" resultType="com.cloud.productprovider.composite.DefaultProvider"> select a.id,a.name,b.code from product_provider a inner join default_provider b on a.id=b.id <where> a.id=#{default_provider_id} </where> </select>
當然我們的重點還是otherValues這裡
<association property="otherValues" javaType="com.nh.micro.ext.ExtBeanWrapper" column="other_property_value"> <id property="entry" column="other_property_value" jdbcType="VARCHAR" typeHandler="com.nh.micro.ext.th.TagToJsonTypeHandler" /> </association>
獲取資料的全部select程式碼如下
<select id="findProductById" parameterType="java.lang.Long" resultMap="productMap" resultType="com.cloud.productprovider.composite.ProviderProduct"> select id,code,name,model,brand_id,normal_price,level_id,default_provider_id,other_property_value from product <where> id=#{id} </where> </select>
獲取出來的資料如下
{
"code": 200,
"data": {
"brand": {
"code": "001",
"id": 1,
"logoUrl": "http://123.456.789",
"name": "飛利浦",
"sort": 1
},
"code": "0001",
"levelName": "高階項鍊",
"otherValues": {
"innerMap": {
"2459623566996411192": "國際",
"2459623566996408120": "10",
"2459623566996409144": "呼和浩特",
"2459623566996410168": "飛利浦",
"2459623566996412216": "包郵"
},
"obj": {
"2459623566996410168": "飛利浦",
"2459623566996411192": "國際",
"2459623566996408120": "10",
"2459623566996409144": "呼和浩特",
"2459623566996412216": "包郵"
}
},
"product": {
"id": 2459722970793247544,
"model": "AAAAA",
"name": "AAAA",
"onShelf": false,
"price": {
"begin": false,
"normalPrice": 199
}
},
"provider": {
"code": "0001",
"productProvider": {
"id": 2459698718186668856,
"name": "大眾4S店",
"productList": []
},
"status": false
}
},
"msg": "操作成功"
}
當然我們這裡要把其他屬性的id替換成使用者能看懂的其他屬性的名稱
@Override public Provider findProduct(Long id) { ProductDao productDao = SpringBootUtil.getBean(ProductDao.class); OtherPropertyDao otherPropertyDao = SpringBootUtil.getBean(OtherPropertyDao.class); Provider product = productDao.findProductById(id); Map map = ((ProviderProduct) product).getOtherValues().getInnerMap(); Map<String,String> insteadMap = new HashMap<>(); for (Object key : map.keySet()) { log.info("鍵名為:" + String.valueOf(key)); String name = otherPropertyDao.findNameById(Long.parseLong(String.valueOf(key))); insteadMap.put(name,(String) map.get(key)); } ((ProviderProduct) product).getOtherValues().setObj(insteadMap); return product; }
最後我們獲取的結果為
{
"code": 200,
"data": {
"brand": {
"code": "001",
"id": 1,
"logoUrl": "http://123.456.789",
"name": "飛利浦",
"sort": 1
},
"code": "0001",
"levelName": "高階項鍊",
"otherValues": {
"innerMap": {
"商品等級": "國際",
"運費設定": "包郵",
"生產廠家": "飛利浦",
"包裝規格": "10",
"商品產地": "呼和浩特"
},
"obj": {
"$ref": "$.data.otherValues.innerMap"
}
},
"product": {
"id": 2459722970793247544,
"model": "AAAAA",
"name": "AAAA",
"onShelf": false,
"price": {
"begin": false,
"normalPrice": 199
}
},
"provider": {
"code": "0001",
"productProvider": {
"id": 2459698718186668856,
"name": "大眾4S店",
"productList": []
},
"status": false
}
},
"msg":