扩展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

原文地址:https://www.cnblogs.com/lookup/p/11523409.html

时间: 2024-10-04 10:17:38

扩展mybatis和通用mapper,支持mysql的geometry类型字段的相关文章

Spring Boot集成MyBatis实现通用Mapper

前言 MyBatis关于MyBatis,大部分人都很熟悉.MyBatis 是一款优秀的持久层框架,它支持定制化 SQL.存储过程以及高级映射.MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集.MyBatis 可以使用简单的 XML 或注解来配置和映射原生信息,将接口和 Java 的 POJOs(Plain Old Java Objects,普通的 Java对象)映射成数据库中的记录.不管是DDD(Domain Driven Design,领域驱动建模)还是分层架构的风

教你如何开发Mybatis的通用Mapper

本文档地址: 如何开发自己的通用Mapper 博客排版不如直接在gitosc上查看,建议去上面的链接查看. 前言 自从发了通用Mapper-0.1.0版本后,我觉得对少数人来说,这可能是他们正好需要的一个工具.至少目前的通用DAO中,很少能有比这个更强大的. 但是对另一部分人来说,使用Mybatis代码生成器(我正在和一些朋友翻译这个文档,地址:MyBatis Generator)生成xml很方便,不需要使用通用Mapper. 实际上如果你无法在自己的业务中提取出通用的单表(多表实际上能实现,但

Spring Boot集成Mybatis及通用Mapper

集成Mybatis可以通过 mybatis-spring-boot-starter 实现. <!-- https://mvnrepository.com/artifact/org.mybatis.spring.boot/mybatis-spring-boot-starter --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-

mybatis使用通用mapper步骤

首先在pom.xml文件中通过maven下载通用mapper包 创建通用mapper接口且继承Mapper<T>类泛型 在mybatis配置文件中引入mapperclass 接下来便开始在通用mapper的世界中畅游了~~~~~~~~~ package com.aaa.entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.Table; impo

mybatis的通用mapper小结

import tk.mybatis.mapper.entity.Example; //此包是tk下的1.定义一个dao层接口不需要任何方法 需要继承Mapper<类型> 2.在service中注入dao import com.alibaba.dubbo.config.annotation.Service; import com.github.pagehelper.Page; import com.github.pagehelper.PageHelper;import org.springfra

Mybatis之通用mapper使用注解的方式写动态sql-小结

使用注解的方式直接在语句中拼写动态SQL语句 注意事项: @Select("<script> SELECT DISTINCT project_id FROM `project_partner` WHERE belong=1 AND project_id " + "IN " + "<foreach item='item' index='index' collection='ids' open='(' close=')' separator=

mysql 中int类型字段unsigned和signed的探索

转自:http://www.0791quanquan.com/news_keji/topic_816453/ 探索一:正负数问题 拿tinyint字段来举例,unsigned后,字段的取值范围是0-255,而signed的范围是-128 - 127. 那么如果我们在明确不需要负值存在的情况下,通常是不要设置signed来支持负数的. 因为只支持正数会让存储空间大一倍呢(当然我这种表达可能不准确). 假设我们使用tinyint来存储一些状态值. 0表示删除,1表示待付款,2表示已付款,3....

java Date时间的各种转换方式和Mysql存时间类型字段的分析

一:各种Date之间的转换方法 public class TimeTest { public static void main(String[] args) { Date date = new Date(); // 一.获取当前系统时间和日期并格式化输出: SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//设置日期格式 String dateTime = df.format(date); // For

往MySQL数据库datetime类型字段中插入当前时间

代码: StringBuilder sb = new StringBuilder(); sb.append(" insert into uosdetailfile ("); sb.append(" filename, "); sb.append(" content,"); sb.append(" addtime "); sb.append(" ) values ("); sb.append(" '