1. 程式人生 > >Mybatis操作mysql 8的Json欄位型別

Mybatis操作mysql 8的Json欄位型別

Json欄位是從mysql 5.7起加進來的全新的欄位型別,現在我們看看在什麼情況下使用該欄位型別,以及用mybatis如何操作該欄位型別

一般來說,在不知道欄位的具體數量的時候,使用該欄位是非常合適的,比如說——商品的無限屬性。

現在我們來假設這麼一個場景,在商品的二級分類中給商品定義足夠多的屬性,我們先設計屬性的類

/**
 * 商品自定義屬性
 */
@NoArgsConstructor
@AllArgsConstructor
public class OtherProperty implements Serializable {
    @Getter
    @Setter
private Long id; //屬性id @Getter @Setter private FormType formType; //前端使用的表單型別 @Getter @Setter private String name; //屬性名稱 @Getter @Setter private String unit; //單位 @Getter @Setter private String values; //可選值以@分隔,如配件@車品 @Getter private
List<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":