DTeam 团队日志

Doer、Delivery、Dream

当Grails-GORM遇上PostGIS

胡键 Posted at — Dec 14, 2018 阅读

由于近期项目性质的缘故,我们用到了 PostGIS。又因为我们后端用到的工具比较特殊且在国内相对少见,故有必要写一下。

先说说我们的工具:

知道 Grails 的人都知道 GORM 的妙处,并且由于 Hibernate 现在已经将 Hibernate Spatial 纳入官方发行包,而后者已经支持 PostGIS,按理只需按照文档进行配置就好了。可问题就出在由于使用了 grails pg extensions 插件,其方言和支持 PostGIS 的方言有冲突,而我又是一个贪心的人,希望能同时获得两者的好处,自然就得折腾一番。

好在,问题最终得以解决,现将解决方案随手记下,灌水一篇。

引入 PostGIS 的相关依赖

在 build.gralde 中,添加如下两行:

compile "org.hibernate:hibernate-spatial:5.2.17.Final"
compile "com.vividsolutions:jts:1.13"

注意其版本与 hibernate-core 保持一致。

修改方言

根据 Hibernate 的文档所述,需要采用新的 PostgisDialect 来替代原有的 PG 方言方能在 Domain Class 中使用 PostGIS 的类型。这下问题来了,由于 Grails PG Extensions 插件本身引入了一些类型扩展,如数组、jsonb 等,也重新引入了一个新的方言:net.kaleidos.hibernate.PostgresqlExtensionsDialect。

如何避免二选一呢?一个偷巧的方法就是:重新定义一个方言。因为 PostgresqlExtensionsDialect 和 PostgisPG94Dialect 都扩展了 PostgreSQL94Dialect,并且两个方言不过都是对现有 Hibernate 类型的扩充。既然如此,那就新增加一个方言:

@CompileStatic
class Postgis94ExtensionsDialect extends PostgisPG94Dialect {

    private static final String SEQUENCE_PER_TABLE = 'dataSource.postgresql.extensions.sequence_per_table'

    /**
     * Register postgresql types
     */
    Postgis94ExtensionsDialect() {
        super()
        registerColumnType(Types.ARRAY, 'array')
        registerColumnType(ArrayType.LONG_ARRAY, '_int8')
        registerColumnType(ArrayType.INTEGER_ARRAY, '_int4')
        registerColumnType(ArrayType.ENUM_INTEGER_ARRAY, '_int4')
        registerColumnType(ArrayType.STRING_ARRAY, '_varchar')
        registerColumnType(ArrayType.DOUBLE_ARRAY, '_float8')
        registerColumnType(ArrayType.FLOAT_ARRAY, '_float4')
        registerColumnType(ArrayType.UUID_ARRAY, '_uuid')
        registerColumnType(HstoreMapType.SQLTYPE, 'hstore')
        registerColumnType(JsonMapType.SQLTYPE, 'json')
        registerColumnType(JsonbMapType.SQLTYPE, 'jsonb')
    }
}

以新的 PostgisPG94Dialect 为父类就绕过了上面的问题。

配置

接下来就简单了,在 application.yml 中修改 Hibernate 的方言:

hibernate:
  dialect: …….Postgis94ExtensionsDialect

既然配置好了,那就测试一下呗。

测试

作为一个有自我要求的程序员,当然得写自动化测试,:)

测试用的 Domain Class:

class MyDomain {

    Map kvPair
    String[] strings
    Point location
    LocalDateTime dateCreated

    static mapping = {
        kvPair comment: 'Jsonb示例', type: JsonbMapType
        strings comment: '数组示例', type: ArrayType, params: [type: String]
        location comment: '位置信息'
    }
}

上述的 PostGIS 会被映射到 PostGIS 的 geometry 类型,其他由 grails pg extensions 插件引入的类型也会被映射到对应的类型。下面是 GORM 自动生成的数据库表:

# \d my_domain
                                         Table "public.my_domain"
    Column    |            Type             | Collation | Nullable |                Default
--------------+-----------------------------+-----------+----------+---------------------------------------
 id           | bigint                      |           | not null | nextval('my_domain_id_seq'::regclass)
 version      | bigint                      |           | not null |
 date_created | timestamp without time zone |           | not null |
 strings      | character varying[]         |           | not null |
 location     | geometry                    |           | not null |
 kv_pair      | jsonb                       |           | not null |
Indexes:
    "my_domain_pkey" PRIMARY KEY, btree (id)

然后是测试 Spec:

void 'test something'() {
    setup:
    myDomainService.save(new MyDomain(kvPair: [key: 'value']
            , strings: ['1', '2'].toArray()
            , location: new GeometryFactory().createPoint(new Coordinate(10, 5))))

    when:
    MyDomain myDomain = MyDomain.list()[0]

    then:
    myDomain.dateCreated
    myDomain.kvPair.key == 'value'
    myDomain.strings == ['1', '2']
    myDomain.location.x == 10
    myDomain.location.y == 5
}

测试结果毫无悬念的通过。

最后,假如你也用 vagrant 做开发,可以参考我的这个脚本来做 provision。