擴展mybatis和通用mapper,支持mysql的geometry類型字段


因項目中需要用到地理位置信息的存儲、查詢、計算等,經過研究決定使用mysql(5.7版本)數據庫的geometry類型字段來保存地理位置坐標,使用虛擬列(Virtual Generated Column)來保存geohash值,便於查詢。
需要了解geometry如何使用及優勢可參看:
mysql中geometry類型的簡單使用
MySQL Geometry擴展在地理位置計算中的效率優勢

本文主要講解擴展mybatis和通用mapper,使其支持geometry類型字段的新增、修改、查詢

首先創建一張表,作為本文的案例

CREATE TABLE `t_user` (
  `id` varchar(45) NOT NULL,
  `name` varchar(10) NOT NULL COMMENT '姓名',
  `gis` geometry NOT NULL COMMENT '空間位置信息',
  `geohash` varchar(20)  GENERATED ALWAYS AS (st_geohash(`gis`,8)) VIRTUAL NOT NULL COMMENT 'geo哈希',
  PRIMARY KEY (`id`),
  UNIQUE KEY `id` (`id`),
  SPATIAL KEY `idx_gis` (`gis`),
  KEY `idx_geohash` (`geohash`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用戶';

創建對應的實體類

@Table(name = "t_user")
public class User {
    private String id;
    private String name;
    @Column
    private GeoPoint gis;
    @VirtualGenerated
    private String geohash;
}

其中GeoPoint類型是我們自定義的類型,用來對應mysql的geometry類型

public class GeoPoint {
    public GeoPoint(BigDecimal lng, BigDecimal lat) {
        this.lng = lng;
        this.lat = lat;
    }
    /* 經度 */
    private BigDecimal lng;
    /* 緯度 */
    private BigDecimal lat;
}

@VirtualGenerated注解是我們自定義的注解,用來標識虛擬列字段,使insert、update時能夠忽略該字段

使tk通用mapper的insert支持geometry類型

tk通用mapper默認生成的insert語句xml是這樣

<insert>
    INSERT INTO t_user 
    <trim prefix="(" suffix=")" suffixOverrides=",">
        <if test="id != null">id,</if>
        <if test="name != null">name,</if>
        <if test="gis != null">gis,</if>
    </trim>
    <trim prefix="VALUES(" suffix=")" suffixOverrides=",">
        <if test="id != null">#{id},</if>
        <if test="name != null">#{name},</if>
        <if test="gis != null">#{gis},</if>
    </trim>
</insert>

而我們希望生成的insert語句xml是這樣

<insert>
    INSERT INTO t_user 
    <trim prefix="(" suffix=")" suffixOverrides=",">
        <if test="id != null">id,</if>
        <if test="name != null">name,</if>
        <if test="gis != null">gis,</if>
    </trim>
    <trim prefix="VALUES(" suffix=")" suffixOverrides=",">
        <if test="id != null">#{id},</if>
        <if test="name != null">#{name},</if>
        <if test="gis != null">geomfromtext('point(${gis.lng} ${gis.lat})'),</if>
    </trim>
</insert>

於是...開始我們的修改,查看通用mapper的源碼得知,通用insert主要是通過BaseInsertMapper和BaseInsertProvider這兩個類實現的,所以我們仿造着創建GeoBaseInsertMapper.java 和 GeoBaseInsertProvider.java,其中GeoBaseInsertProvider.java直接復制BaseInsertProvider來修改即可
GeoBaseInsertMapper.java如下:

@RegisterMapper
public interface GeoBaseInsertMapper<T> {
    @InsertProvider(type = GeoBaseInsertProvider.class, method = "dynamicSQL")
    int insert(T record);

    @InsertProvider(type = GeoBaseInsertProvider.class, method = "dynamicSQL")
    int insertSelective(T record);
}

最主要的是GeoBaseInsertProvider.java

public class GeoBaseInsertProvider extends MapperTemplate {

    public GeoBaseInsertProvider(Class<?> mapperClass, MapperHelper mapperHelper) {
        super(mapperClass, mapperHelper);
    }

    public String insert(MappedStatement ms) {
        Class<?> entityClass = getEntityClass(ms);
        StringBuilder sql = new StringBuilder();
        //獲取全部列
        Set<EntityColumn> columnList = EntityHelper.getColumns(entityClass);
        EntityColumn logicDeleteColumn = SqlHelper.getLogicDeleteColumn(entityClass);
        processKey(sql, entityClass, ms, columnList);
        sql.append(SqlHelper.insertIntoTable(entityClass, tableName(entityClass)));
        sql.append("<trim prefix=\"(\" suffix=\")\" suffixOverrides=\",\">");
        //當某個列有主鍵策略時,不需要考慮他的屬性是否為空,因為如果為空,一定會根據主鍵策略給他生成一個值
        for (EntityColumn column : columnList) {
            if (!column.isInsertable()) {
                continue;
            }
            //忽略虛擬列
            if (column.getEntityField().isAnnotationPresent(VirtualGenerated.class)) {
                continue;
            }
            sql.append(column.getColumn() + ",");
        }
        sql.append("</trim>");
        sql.append("<trim prefix=\"VALUES(\" suffix=\")\" suffixOverrides=\",\">");
        for (EntityColumn column : columnList) {
            if (!column.isInsertable()) {
                continue;
            }
            //忽略虛擬列
            if (column.getEntityField().isAnnotationPresent(VirtualGenerated.class)) {
                continue;
            }
            if (logicDeleteColumn != null && logicDeleteColumn == column) {
                sql.append(SqlHelper.getLogicDeletedValue(column, false)).append(",");
                continue;
            }

            //優先使用傳入的屬性值,當原屬性property!=null時,用原屬性
            //自增的情況下,如果默認有值,就會備份到property_cache中,所以這里需要先判斷備份的值是否存在
            if (column.isIdentity()) {
                sql.append(SqlHelper.getIfCacheNotNull(column, column.getColumnHolder(null, "_cache", ",")));
            } else {
                //判斷字段是GeoPoint類型時,調用getGeoColumnHolder方法來生成
                if (column.getJavaType() == GeoPoint.class) {
                    //<if test="property != null">geomfromtext('point(108.9498710632 34.2588125935)'),</if>
                    sql.append(SqlHelper.getIfNotNull(column, getGeoColumnHolder(column), isNotEmpty()));
                } else {
                    //其他情況值仍然存在原property中
                    sql.append(SqlHelper.getIfNotNull(column, column.getColumnHolder(null, null, ","), isNotEmpty()));
                }

            }
            //當屬性為null時,如果存在主鍵策略,會自動獲取值,如果不存在,則使用null
            if (column.isIdentity()) {
                sql.append(SqlHelper.getIfCacheIsNull(column, column.getColumnHolder() + ","));
            } else {
                //判斷字段是GeoPoint類型時,調用getGeoColumnHolder方法來生成
                if (column.getJavaType() == GeoPoint.class) {
                    //<if test="property == null">geomfromtext('point(108.9498710632 34.2588125935)'),</if>
                    sql.append(SqlHelper.getIfIsNull(column, getGeoColumnHolder(column), isNotEmpty()));
                } else {
                    //當null的時候,如果不指定jdbcType,oracle可能會報異常,指定VARCHAR不影響其他
                    sql.append(SqlHelper.getIfIsNull(column, column.getColumnHolder(null, null, ","), isNotEmpty()));
                }
            }
        }
        sql.append("</trim>");
        return sql.toString();
    }

    /*
     * insert GEO字段占位符
     */
    private String getGeoColumnHolder(EntityColumn column){
        return String.format("geomfromtext('point(${%s.lng} ${%s.lat})'),",column.getProperty(),column.getProperty());
    }

    //忽略以下部分代碼

}

讓你的mapper接口繼承GeoBaseInsertMapper 就能使insert方法支持geometry類型了,同時能夠忽略虛擬列。

@Repository
public interface UserMapper extends GeoBaseInsertMapper<User>{
}

如果你理解了通用insert的修改,update的修改也同樣如此,相信難不倒你,這里就不再貼代碼了。

使mybatis查詢支持將geometry類型字段映射到GeoPoint類型

mybatis通過定義typeHandler將數據類型映射為java類型,mybatis內置了多種常見的typeHandler,但沒有支持geometry,好在mybatis提供了足夠的擴展性,我們可以自定義typeHandler,這里還需要在pom.xml引入jts庫來解析

<dependency>
    <groupId>com.vividsolutions</groupId>
    <artifactId>jts</artifactId>
    <version>${jts.version}</version>
</dependency>

接下來是自定義的MysqlGeoPointTypeHandler

/*
 * mybatis查詢結果集中 mysql的geometry類型映射到GeoPoint對象
 */
@MappedTypes(value = {GeoPoint.class})
public class MysqlGeoPointTypeHandler extends BaseTypeHandler<GeoPoint> {

    private WKBReader _wkbReader;

    public MysqlGeoPointTypeHandler(int srid) {
        GeometryFactory _geometryFactory = new GeometryFactory(new PrecisionModel(), srid);
        _wkbReader = new WKBReader(_geometryFactory);
    }

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, GeoPoint parameter, JdbcType jdbcType) {
        //因為GeoPoint對象里包含經度和緯度兩個值,無法直接適配到一個參數,所以也不會使用到這個方法
    }

    @Override
    public GeoPoint getNullableResult(ResultSet rs, String columnName) throws SQLException {
        return fromMysqlWkb(rs.getBytes(columnName));
    }

    @Override
    public GeoPoint getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        return fromMysqlWkb(rs.getBytes(columnIndex));
    }

    @Override
    public GeoPoint getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        return fromMysqlWkb(cs.getBytes(columnIndex));
    }

    /*
     * bytes轉GeoPoint對象
     */
    private GeoPoint fromMysqlWkb(byte[] bytes) {
        if (bytes == null) {
            return null;
        }
        try {
            byte[] geomBytes = ByteBuffer.allocate(bytes.length - 4).order(ByteOrder.LITTLE_ENDIAN)
                    .put(bytes, 4, bytes.length - 4).array();
            Geometry geometry = _wkbReader.read(geomBytes);
            Point point = (Point) geometry;
            return new GeoPoint(new BigDecimal(String.valueOf(point.getX())), new BigDecimal(String.valueOf(point.getY())));
        } catch (Exception e) {
        }
        return null;
    }
}

然后我們需要將MysqlGeoPointTypeHandler添加到mybatis配置中,這樣mybatis在遇到GeoPoint時就知道怎么映射了。
這里演示用java代碼來配置mybatis,也可以在mybatis.xml文件中配置

@Configuration
@MapperScan(basePackages = {"com.carson.**.mapper"}, sqlSessionTemplateRef = "sqlSessionTemplate")
public class MybatisConfig {

    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dataSource);
        bean.setVfs(SpringBootVFS.class);
        //添加XML目錄
        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        try {
            bean.setMapperLocations(resolver.getResources("classpath:mybatis/**/*Mapper.xml"));
            bean.setTypeAliasesPackage("com.carson.pojo");
            //添加MysqlGeoPointTypeHandler
            bean.setTypeHandlers(new TypeHandler[]{new MysqlGeoPointTypeHandler()});
            bean.getObject().getConfiguration().setMapUnderscoreToCamelCase(true);
            return bean.getObject();
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }
}

完成這些以后查詢的結果集里包含geometry類型的字段,就能映射到GeoPoint了,從而可以獲取經緯度

源碼在哪里? talk is cheap,show me the code!

如果你懶得看以上長篇大論,只想要開箱即用的代碼,就在這里了,有幫助的話記得給個star哦!

https://github.com/tzjzcy/mybatis-mysql-geo-boot

https://gitee.com/tzjzcy/mybatis-mysql-geo-boot


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM