【Java 实战】 MybatisPlus 实体类中自定义字段类型

283 阅读3分钟

情景描述

实体类

@Data
@SuperBuilder
@TableName("layers_media_layer_geotif_meta")
@AllArgsConstructor
public class GeotifMetaDO {

  /**
   * id
   */
  @TableId
  private String id;

  
  /**
   * 文件名称
   */
  @TableField("file_name")
  private String fileName;


  /**
   * 空间范围
   */
  private Extent extent;

}

GeotifMetaDO 是一个数据对象对应表名 layers_media_layer_geotif_meta

自定义字段类

注意, Extent 是一个自定义的类,来描述空间范围

/**
 * 空间范围
 */
@Data
@SuperBuilder
@Jacksonized
public class Extent {
    /**
     * 最小经度
     */
    @Builder.Default
    private double xmin = -180.0;
    /**
     * 最小纬度
     */
    @Builder.Default
    private double ymin = -90.0;
    /**
     * 最大经度
     */
    @Builder.Default
    private double xmax = 180.0;

    /**
     * 最大纬度
     */
    @Builder.Default
    private double ymax = 90.0;
}

数据库中的对应列

某些数据库,提供了空间字段的扩展。将数据存储成空间类型,便于日后执行空间计算。

这里以 集成了 PostGIS 的 PG 数据库为例。

ALTER TABLE IF EXISTS public.layers_media_layer_geotif_meta
    ADD COLUMN extent polygon;

使用 polygon 作为类型,来存储 extent

插入操作

构建 Mapper

package cn.vunk.geotif.MediaLayer.biz.dal.pgsql;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import cn.vunk.geotif.MediaLayer.biz.dal.dataobject.GeotifMetaDO;

public interface GeotifMetaMapper extends BaseMapper<GeotifMetaDO> {}

执行测试

@SpringBootTest
public class MapperTest {
  
  @Resource
  private GeotifMetaMapper geotifMetaMapper;

  @Test 
  public void geotifMetaAdd () {
    var extent = Extent.builder()
      .xmin(0.0)
      .ymin(0.0)
      .xmax(10.0)
      .ymax(10.0)
      .build();

    var geotifMeta = GeotifMetaDO.builder()
      .fileName("test.tif")
      .extent(extent)
      .build();

    geotifMetaMapper.insert(geotifMeta);
  }
}

发生错误

Caused by: java.lang.IllegalStateException: Type handler was null on parameter mapping for property 'extent'. It was either not specified and/or could not be found for the javaType (cn.vunk.esri.core.geometry.Extent) : jdbcType (null) combination.

TypeHandler

实现

全局配置扫描路径

mybatis-plus:
  type-handlers-package: cn.vunk.esri.framework.ibatis

实现 ExtentTypeHandler


package cn.vunk.esri.framework.ibatis;

import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedTypes;

import cn.vunk.esri.core.geometry.Extent;
import net.postgis.jdbc.PGgeometry;
import net.postgis.jdbc.geometry.LinearRing;
import net.postgis.jdbc.geometry.Point;
import net.postgis.jdbc.geometry.Polygon;

import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;


@MappedTypes(Extent.class)
public class ExtentTypeHandler extends BaseTypeHandler<Extent> {

    @Override
    public void setNonNullParameter(
      PreparedStatement ps, 
      int i, 
      Extent parameter, 
      JdbcType jdbcType
    ) throws SQLException {

      Point[] points = new Point[] {
        new Point(parameter.getXmin(), parameter.getYmin()),
        new Point(parameter.getXmax(), parameter.getYmin()),
        new Point(parameter.getXmax(), parameter.getYmax()),
        new Point(parameter.getXmin(), parameter.getYmax())
      };
      LinearRing linearRing = new LinearRing(points);
      Polygon polygon = new Polygon(new LinearRing[] { linearRing });
      PGgeometry geom = new PGgeometry(polygon);
      

      ps.setString(i, geom.getValue());

    }
	// ...

}

测试与错误

### Error updating database.  Cause: org.postgresql.util.PSQLException: 错误: 字段 "extent" 的类型为 polygon, 但表达式的类型为 character varying
  建议:你需要重写或转换表达式
  位置:90
### The error may exist in cn/vunk/geotif/MediaLayer/biz/dal/pgsql/GeotifMetaMapper.java (best guess)
### The error may involve cn.vunk.geotif.MediaLayer.biz.dal.pgsql.GeotifMetaMapper.insert-Inline
### The error occurred while setting parameters
### SQL: INSERT INTO layers_media_layer_geotif_meta  ( id, file_name, extent )  VALUES (  ?, ?, ?  )
### Cause: org.postgresql.util.PSQLException: 错误: 字段 "extent" 的类型为 polygon, 但表达式的类型为 character varying
  建议:你需要重写或转换表达式
  位置:90

关键问题

实现上述自定义字段能够成功插入数据库。 是 ExtentTypeHandler 遗漏了什么吗?

如何解决最后的报错

### Error updating database.  Cause: org.postgresql.util.PSQLException: 错误: 字段 "extent" 的类型为 polygon, 但表达式的类型为 character varying
  建议:你需要重写或转换表达式
  位置:90

解决方案

插入时,使用 setObject 插入 PGpolygon 实例

import org.postgresql.geometric.PGpolygon;
// ...
@Override
public void setNonNullParameter(
  PreparedStatement ps, 
  int i, 
  Extent parameter, 
  JdbcType jdbcType
) throws SQLException {
  PGpoint[] pgpoints = new PGpoint[4];
  pgpoints[0] = new PGpoint(parameter.getXmin(), parameter.getYmin());
  pgpoints[1] = new PGpoint(parameter.getXmax(), parameter.getYmin());
  pgpoints[2] = new PGpoint(parameter.getXmax(), parameter.getYmax());
  pgpoints[3] = new PGpoint(parameter.getXmin(), parameter.getYmax());
  PGpolygon pgpolygon = new PGpolygon(pgpoints);
  ps.setObject(i, pgpolygon);
}

简单查看 PGpolygon 的实现

image-20240424144120973.png

    public PGpolygon() {
      this.type = "polygon";
   	}

	public @Nullable String getValue() {
      PGpoint[] points = this.points;
      if (points == null) {
         return null;
      } else {
         StringBuilder b = new StringBuilder();
         b.append("(");

         for(int p = 0; p < points.length; ++p) {
            if (p > 0) {
               b.append(",");
            }

            b.append(points[p].toString());
         }

         b.append(")");
         return b.toString();
      }
   }

这是实际上在指导框架,如何执行 sql 将对应数据插入数据库。

完整的实现


package cn.vunk.esri.framework.ibatis;

import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import org.postgresql.geometric.PGpoint;
import org.postgresql.geometric.PGpolygon;

import cn.vunk.esri.core.geometry.Extent;

import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;


@MappedTypes(Extent.class)
@MappedJdbcTypes(JdbcType.OTHER)
public class ExtentTypeHandler extends BaseTypeHandler<Extent> {

    @Override
    public void setNonNullParameter(
      PreparedStatement ps, 
      int i, 
      Extent parameter, 
      JdbcType jdbcType
    ) throws SQLException {
      PGpoint[] pgpoints = new PGpoint[4];
      pgpoints[0] = new PGpoint(parameter.getXmin(), parameter.getYmin());
      pgpoints[1] = new PGpoint(parameter.getXmax(), parameter.getYmin());
      pgpoints[2] = new PGpoint(parameter.getXmax(), parameter.getYmax());
      pgpoints[3] = new PGpoint(parameter.getXmin(), parameter.getYmax());
      PGpolygon pgpolygon = new PGpolygon(pgpoints);
      ps.setObject(i, pgpolygon);
    }

    private Extent stringToExtent (String string) throws SQLException {

      var polygon = new PGpolygon(string);
      var extent = Extent.builder()
        .xmin(polygon.points[0].x)
        .ymin(polygon.points[0].y)
        .xmax(polygon.points[2].x)
        .ymax(polygon.points[2].y)
        .build();
      return extent;
    }
    
    @Override
    public Extent getNullableResult(
      ResultSet rs, 
      String columnName
    ) throws SQLException {
        // 从ResultSet中获取数据库字段的值,并将其转换为Extent对象
        // 例如,从字符串形式的值中解析出Extent对象
        String extentValue = rs.getString(columnName);
        return stringToExtent(extentValue);
    }

    @Override
    public Extent getNullableResult(
      ResultSet rs, 
      int columnIndex
    ) throws SQLException {
      String string = rs.getString(columnIndex);
      return stringToExtent(string);
    }

    @Override
    public Extent getNullableResult(
      CallableStatement cs, 
      int columnIndex
    ) throws SQLException {
      String string = cs.getString(columnIndex);
      return stringToExtent(string);
    }


}