使用Gorm和MySQL处理空间数据

使用Gorm和MySQL处理空间数据,mysql,go,spatial,go-gorm,Mysql,Go,Spatial,Go Gorm,我参考了关于支持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"`

我参考了关于支持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)'))

如果我使用自己的
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_geomefromtext()
在内的整个值在Gorm的单引号中引用,因此它不起作用:

INSERT INTO `tag` (`name`,`loc`) VALUES ('tag name','ST_GeomFromText('POINT(10.000000 20.000000)')');
我如何让它工作

编辑1:

我追踪Gorm代码,最终得到了
callback\u create.go
createCallback
函数。在它内部检查
,如果primaryField==nil
且为真,则它进入调用
scope.SQLDB().Exec
,然后我无法进一步跟踪

scope.SQL是字符串
插入
标记
name
loc
)值(?,)
scope.SQLVars
打印
[标记名{{12[10 20]0}]
。看起来插值发生在这个调用中

这是对
数据库/sql
代码的调用吗

编辑2:


发现了一个类似的问题。但我不理解解决方案。

更新:这种方法不起作用。

可以在Gorm生成sql之前将列设置为a

例如,插入之前的类似内容:

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
}

这是另一种方法;使用二进制编码

根据这一点,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) |
+--------+------------------------+
  • (4326是存储全球范围内参考数据的最常用空间参考。它是PostGIS空间数据库和GeoJSON标准的默认值。大多数web地图库默认也使用它。)

你能用吗?@Mark我可以,但我绝对不想用。我有一个已经定义了CRUD的REST服务。如果这是我认为应该的方式,那么一旦我定义了模式,就不需要为每个表复制所有端点处理程序,从JSON到JSON的编组/解编组只是一点工作。为什么原始
选择LAST\u INSERT\u ID()
不能满足在原始查询中使用auto\u inc的要求?它与连接绑定,因此不会受到其他连接的干扰。GORM是否无法“转义”引号以便您可以使用嵌套引号?无论如何,在大多数(如果不是全部的话)MySQL语法情况下,单引号和双引号是可互换的。这就提供了两层,而不需要任何转义。@我认为嵌套引号不起作用。db函数不应该被引用。关于
last\u insert\u id()。谢谢你的提示。:)奇怪的是,在本例中,我的自定义扫描程序是用参数*gorm.SqlExpr调用的。我不知道该怎么办。在查询过程中需要此扫描仪。注释也无助于创建数据。我收到
错误1364:字段“loc”没有默认值
。我在添加钩子之前添加了跟踪结果。
字段“loc”没有默认值
如果列在db中不为null,但未设置为插入(例如,忽略/未导出),则会发生该错误,并且Gorm似乎需要在模型上设置一个默认值作为属性。在为insert准备值时调用Scan,我不知道为什么,但假设这是为了双重检查。尽管可以调用SetColumn来设置*SqlExpr,但在插入时不使用它。我认为如果字段的原始类型是*SqlExpr,则会使用它。很抱歉,这种方法似乎行不通。另一个想法可能是创造你自己的方言。马克,你知道133行在哪里吗?我正在尝试查找scope.SQLDB().Exec()的定义。我正试图找到那个额外的单引号是在哪里构建的。如果我能弄明白,我希望我能定制它。这是方言的作品吗?非常感谢你的详细解释。结果很好。没有你的帮助,我不可能弄明白。还有一个
编码/wekb
,但是值非常接近0,我不知道为什么。我还尝试使用类型转换而不是封装,但是来自扫描接口的缓冲区(src)的长度似乎是错误的。但是你的方法很管用。别担心,希望能有帮助。
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) |
+--------+------------------------+