关于Gorm配合Postgim的使用

发布时间 2023-10-20 20:48:07作者: Yeuoly

碰到一个问题,项目中需要引入坐标系统,而数据库选用是postgresql,那么理所当然的想到的就是用postgim插件,关于这个插件的使用,我们建议使用docker,doccker-compose配置如下

version: '3.1'
services:
  db:
    image: postgis/postgis:16-3.4
    restart: always
    environment:
      - POSTGRES_PASSWORD=root
    volumes:
      - ~/Database/billboards:/var/lib/postgresql/data
    ports:
      - 5432:5432

直接docker-compose拉起来就就行
随后,编写数据库初始化部分的代码,简单这么操作就行

func InitBillboardsDB(host string, port int, user string, pass string) error {
    dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=billboards sslmode=disable", host, port, user, pass)
    db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
    if err != nil {
        return err
    }

    // install postgim
    err = db.Exec("CREATE EXTENSION IF NOT EXISTS postgis;").Error
    if err != nil {
        return err
    }

    pgsqlDB, err := db.DB()
    if err != nil {
        return err
    }
    pgsqlDB.SetConnMaxIdleTime(time.Minute * 1)

    billboardsDB = db
    return nil
}

比较重要的事中间安装postgim的代码,最后面加了一行用来避免pgsql自动断连的代码
最后一步,定义GeoPoint数据类型,并给Gorm编写插件,类型很简单,就长这样

type GeoPoint struct {
    X, Y float64
}

插件我们参考gorm的文档 https://gorm.io/zh_CN/docs/data_types.html
主要是需要实现几个接口,那么直接写吧

func (g GeoPoint) GormDataType() string {
    return "geometry(Point, 4326)"
}

func (g GeoPoint) GormValue(ctx context.Context, db *gorm.DB) clause.Expr {
    return clause.Expr{
        SQL:  "ST_GeomFromText(?)",
        Vars: []interface{}{fmt.Sprintf("POINT(%f %f)", g.X, g.Y)},
    }
}

func (g *GeoPoint) Scan(value interface{}) error {
    return nil
}

func (g GeoPoint) Value() (driver.Value, error) {
    return fmt.Sprintf("POINT(%f %f)", g.X, g.Y), nil
}

geometry(Point, 4326)类型其实就是地球坐标系,详细定义为:4326 是 EPSG(European Petroleum Survey Group)的一个标识符,它代表了一种地理坐标系,即 WGS 84 坐标系。WGS 84 坐标系是一种用于描述地球表面位置的坐标系,它使用经度和纬度来表示地球上的任意一点。
说白了,就是地球经纬度
在postgim中,ST_GeomFormText可以处理这种坐标,当然,需要先套一层POINT函数用来表示一个点
GormValue和Value分别实现了gorm用于将查询条件转化成SQL语句的功能和gorm用于将原始数据转化为WKT(简单理解,一个字符串,方便数据库读取),这里会有点绕,可以简单理解为Value是go原生sql库里要求提供的一个接口,它用来获取一个类型的字符串值,而GormValue是Gorm在构建Query的时候需要使用到的接口
最后我们来看Scan的实现吧,由于pgsql在读取数据的时候读取到的会是一个16进制字符串,它包含了geometry类型最原始的数据,因此,我们需要将其parse为方便使用的x/y坐标,或者说,lon/lat经纬度

import (
    "github.com/twpayne/go-geom"
    "github.com/twpayne/go-geom/encoding/ewkbhex"
)

func (g *GeoPoint) Scan(value interface{}) error {
    if value == nil {
        return nil
    }

    pt, err := ewkbhex.Decode(value.(string))

    if err == nil {
        if p, ok := pt.(*geom.Point); ok {
            g.X = p.X()
            g.Y = p.Y()
        } else {
            return errors.New(fmt.Sprint("Failed to unmarshal geometry:", pt))
        }
    }

    return err
}

其实实现也就是使用了go-geom库的Decode方法,将原始数据转为geom.Point类型,最后设置x和y,当然了,如果你想,也可以直接使用geom.Point类型,我们这里就没做这个事了,避免耦合度太高