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

识字粗堪供赋役,不须辛苦慕公卿。这篇文章主要讲述扩展mybatis和通用mapper,支持mysql的geometry类型字段相关的知识,希望能为你提供帮助。
因项目中需要用到地理位置信息的存储、查询、计算等,经过研究决定使用mysql(5.7版本)数据库的geometry类型字段来保存地理位置坐标,使用虚拟列(Virtual Generated Column)来保存geohash值,便于查询。 本文主要讲解扩展mybatis和通用mapper,使其支持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>

【扩展mybatis和通用mapper,支持mysql的geometry类型字段】而我们希望生成的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 = https://www.songbingjia.com/android/{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

    推荐阅读