碰到一个问题,项目中需要引入坐标系统,而数据库选用是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类型,我们这里就没做这个事了,避免耦合度太高