使用 Gorm 和 MySQL 处理空间数据
Working with spatial data with Gorm and MySQL
我参考了 关于支持 PostGIS 的空间数据类型。我正在使用 MySQL 并尝试为自定义数据类型 EWKBGeomPoint
.
实现 Value()
我的 Gorm 模型:
import (
"github.com/twpayne/go-geom"
"github.com/twpayne/go-geom/encoding/ewkb"
)
type EWKBGeomPoint geom.Point
type Tag struct {
Name string `json:"name"`
json:"siteID"` // forign key
Loc EWKBGeomPoint `json:"loc"`
}
据我所知,MySQL支持这样的插入:
INSERT INTO `tag` (`name`,`loc`) VALUES ('tag name',ST_GeomFromText('POINT(10.000000 20.000000)'))
或
INSERT INTO `tag` (`name`,`loc`) VALUES ('tag name', ST_GeomFromWKB(X'0101000000000000000000F03F000000000000F03F'))
如果我自己做一个 Value()
来满足 database/sql
的 Valuer
接口:
func (g EWKBGeomPoint) Value() (driver.Value, error) {
log.Println("EWKBGeomPoint value called")
b := geom.Point(g)
bp := &b
floatArr := bp.Coords()
return fmt.Sprintf("ST_GeomFromText('POINT(%f %f)')", floatArr[0], floatArr[1]), nil
}
包括 ST_GeomFromText()
在内的整个值都在 Gorm 的单引号中引用,因此它不起作用:
INSERT INTO `tag` (`name`,`loc`) VALUES ('tag name','ST_GeomFromText('POINT(10.000000 20.000000)')');
如何让它发挥作用?
编辑 1:
我追踪到 Gorm 代码,最终它找到了 callback_create.go
的 createCallback
函数。在它里面检查 if primaryField == nil
是真的,它进入调用 scope.SQLDB().Exec
然后我没有进一步追踪。
scope.SQL 是字符串 INSERT INTO
tag(
name,
loc) VALUES (?,?)
并且 scope.SQLVars
打印 [tag name {{1 2 [10 20] 0}}]
。看起来插值发生在这个调用中。
这是调用 database/sql
代码吗?
编辑 2:
发现了一个类似的 Whosebug 问题 here。但是我不明白解决方案。
更新:此方法无效。
Hooks may let you set the column to a gorm.Expr Gorm 的 sql 代之前。
例如,在插入之前是这样的:
func (t *Tag) BeforeCreate(scope *gorm.Scope) error {
x, y := .... // tag.Loc coordinates
text := fmt.Sprintf("POINT(%f %f)", x, y)
expr := gorm.Expr("ST_GeomFromText(?)", text)
scope.SetColumn("loc", expr)
return nil
}
这是另一种方法;使用二进制编码。
根据此 doc,MySQL 使用 4 个字节存储几何值以指示 SRID(空间参考 ID),后跟值的 WKB(众所周知的二进制)表示形式。
因此一个类型可以使用WKB编码并在Value()和Scan()函数中添加和删除四字节前缀。在其他答案中找到的go-geom库有一个WKB编码包,github.com/twpayne/go-geom/encoding/wkb。
例如:
type MyPoint struct {
Point wkb.Point
}
func (m *MyPoint) Value() (driver.Value, error) {
value, err := m.Point.Value()
if err != nil {
return nil, err
}
buf, ok := value.([]byte)
if !ok {
return nil, fmt.Errorf("did not convert value: expected []byte, but was %T", value)
}
mysqlEncoding := make([]byte, 4)
binary.LittleEndian.PutUint32(mysqlEncoding, 4326)
mysqlEncoding = append(mysqlEncoding, buf...)
return mysqlEncoding, err
}
func (m *MyPoint) Scan(src interface{}) error {
if src == nil {
return nil
}
mysqlEncoding, ok := src.([]byte)
if !ok {
return fmt.Errorf("did not scan: expected []byte but was %T", src)
}
var srid uint32 = binary.LittleEndian.Uint32(mysqlEncoding[0:4])
err := m.Point.Scan(mysqlEncoding[4:])
m.Point.SetSRID(int(srid))
return err
}
使用 MyPoint 类型定义标签:
type Tag struct {
Name string `gorm:"type:varchar(50);primary_key"`
Loc *MyPoint `gorm:"column:loc"`
}
func (t Tag) String() string {
return fmt.Sprintf("%s @ Point(%f, %f)", t.Name, t.Loc.Point.Coords().X(), t.Loc.Point.Coords().Y())
}
正在使用以下类型创建标签:
tag := &Tag{
Name: "London",
Loc: &MyPoint{
wkb.Point{
geom.NewPoint(geom.XY).MustSetCoords([]float64{0.1275, 51.50722}).SetSRID(4326),
},
},
}
err = db.Create(&tag).Error
if err != nil {
log.Fatalf("create: %v", err)
}
MySQL 结果:
mysql> describe tag;
+-------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+-------+
| name | varchar(50) | NO | PRI | NULL | |
| loc | geometry | YES | | NULL | |
+-------+-------------+------+-----+---------+-------+
mysql> select name, st_astext(loc) from tag;
+--------+------------------------+
| name | st_astext(loc) |
+--------+------------------------+
| London | POINT(0.1275 51.50722) |
+--------+------------------------+
- (ArcGIS says 4326 是最常见的空间参考,用于存储整个世界的参考数据。它是 PostGIS 空间数据库和 GeoJSON 标准的默认值。默认情况下也使用它在大多数网络制图库中。)
我参考了 EWKBGeomPoint
.
Value()
我的 Gorm 模型:
import (
"github.com/twpayne/go-geom"
"github.com/twpayne/go-geom/encoding/ewkb"
)
type EWKBGeomPoint geom.Point
type Tag struct {
Name string `json:"name"`
json:"siteID"` // forign key
Loc EWKBGeomPoint `json:"loc"`
}
据我所知,MySQL支持这样的插入:
INSERT INTO `tag` (`name`,`loc`) VALUES ('tag name',ST_GeomFromText('POINT(10.000000 20.000000)'))
或
INSERT INTO `tag` (`name`,`loc`) VALUES ('tag name', ST_GeomFromWKB(X'0101000000000000000000F03F000000000000F03F'))
如果我自己做一个 Value()
来满足 database/sql
的 Valuer
接口:
func (g EWKBGeomPoint) Value() (driver.Value, error) {
log.Println("EWKBGeomPoint value called")
b := geom.Point(g)
bp := &b
floatArr := bp.Coords()
return fmt.Sprintf("ST_GeomFromText('POINT(%f %f)')", floatArr[0], floatArr[1]), nil
}
包括 ST_GeomFromText()
在内的整个值都在 Gorm 的单引号中引用,因此它不起作用:
INSERT INTO `tag` (`name`,`loc`) VALUES ('tag name','ST_GeomFromText('POINT(10.000000 20.000000)')');
如何让它发挥作用?
编辑 1:
我追踪到 Gorm 代码,最终它找到了 callback_create.go
的 createCallback
函数。在它里面检查 if primaryField == nil
是真的,它进入调用 scope.SQLDB().Exec
然后我没有进一步追踪。
scope.SQL 是字符串 INSERT INTO
tag(
name,
loc) VALUES (?,?)
并且 scope.SQLVars
打印 [tag name {{1 2 [10 20] 0}}]
。看起来插值发生在这个调用中。
这是调用 database/sql
代码吗?
编辑 2:
发现了一个类似的 Whosebug 问题 here。但是我不明白解决方案。
更新:此方法无效。
Hooks may let you set the column to a gorm.Expr Gorm 的 sql 代之前。
例如,在插入之前是这样的:
func (t *Tag) BeforeCreate(scope *gorm.Scope) error {
x, y := .... // tag.Loc coordinates
text := fmt.Sprintf("POINT(%f %f)", x, y)
expr := gorm.Expr("ST_GeomFromText(?)", text)
scope.SetColumn("loc", expr)
return nil
}
这是另一种方法;使用二进制编码。
根据此 doc,MySQL 使用 4 个字节存储几何值以指示 SRID(空间参考 ID),后跟值的 WKB(众所周知的二进制)表示形式。
因此一个类型可以使用WKB编码并在Value()和Scan()函数中添加和删除四字节前缀。在其他答案中找到的go-geom库有一个WKB编码包,github.com/twpayne/go-geom/encoding/wkb。
例如:
type MyPoint struct {
Point wkb.Point
}
func (m *MyPoint) Value() (driver.Value, error) {
value, err := m.Point.Value()
if err != nil {
return nil, err
}
buf, ok := value.([]byte)
if !ok {
return nil, fmt.Errorf("did not convert value: expected []byte, but was %T", value)
}
mysqlEncoding := make([]byte, 4)
binary.LittleEndian.PutUint32(mysqlEncoding, 4326)
mysqlEncoding = append(mysqlEncoding, buf...)
return mysqlEncoding, err
}
func (m *MyPoint) Scan(src interface{}) error {
if src == nil {
return nil
}
mysqlEncoding, ok := src.([]byte)
if !ok {
return fmt.Errorf("did not scan: expected []byte but was %T", src)
}
var srid uint32 = binary.LittleEndian.Uint32(mysqlEncoding[0:4])
err := m.Point.Scan(mysqlEncoding[4:])
m.Point.SetSRID(int(srid))
return err
}
使用 MyPoint 类型定义标签:
type Tag struct {
Name string `gorm:"type:varchar(50);primary_key"`
Loc *MyPoint `gorm:"column:loc"`
}
func (t Tag) String() string {
return fmt.Sprintf("%s @ Point(%f, %f)", t.Name, t.Loc.Point.Coords().X(), t.Loc.Point.Coords().Y())
}
正在使用以下类型创建标签:
tag := &Tag{
Name: "London",
Loc: &MyPoint{
wkb.Point{
geom.NewPoint(geom.XY).MustSetCoords([]float64{0.1275, 51.50722}).SetSRID(4326),
},
},
}
err = db.Create(&tag).Error
if err != nil {
log.Fatalf("create: %v", err)
}
MySQL 结果:
mysql> describe tag;
+-------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+-------+
| name | varchar(50) | NO | PRI | NULL | |
| loc | geometry | YES | | NULL | |
+-------+-------------+------+-----+---------+-------+
mysql> select name, st_astext(loc) from tag;
+--------+------------------------+
| name | st_astext(loc) |
+--------+------------------------+
| London | POINT(0.1275 51.50722) |
+--------+------------------------+
- (ArcGIS says 4326 是最常见的空间参考,用于存储整个世界的参考数据。它是 PostGIS 空间数据库和 GeoJSON 标准的默认值。默认情况下也使用它在大多数网络制图库中。)