【问题标题】:Working with spatial data with Gorm and MySQL使用 Gorm 和 MySQL 处理空间数据
【发布时间】:2020-06-16 15:20:09
【问题描述】:

我参考了irbanana's answer 来支持 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/sqlValuer 接口:

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.gocreateCallback 函数。在里面检查if primaryField == nil,这是真的,它会调用scope.SQLDB().Exec,然后我没有进一步追踪。

scope.SQL 是字符串INSERT INTOtag(name,loc) VALUES (?,?)scope.SQLVars 打印[tag name {{1 2 [10 20] 0}}]。看起来插值发生在这个调用中。

这是调用database/sql 代码吗?

编辑 2:

发现了一个类似的 Stackoverflow 问题here。但我不明白解决方案。

【问题讨论】:

  • 你能用raw sql吗?
  • @Mark 我可以,但我绝对不想。我有一个已经定义了 CRUD 的 REST 服务。如果这按我认为应该的方式工作,那么一旦我定义了模式,就不需要为每个表复制所有端点处理程序,并且编组/解编组到 JSON 和从 JSON 编组只是一个小工作。
  • 为什么原始 SELECT LAST_INSERT_ID() 不满足将 auto_inc 用于原始查询的要求?它与连接绑定,因此不会受到其他连接的干扰。
  • GORM 是否无法“转义”引号,以便您可以嵌套引号?无论如何,在大多数(如果不是全部)MySQL 语法情况下,单引号和双引号是可以互换的。这提供了 2 层而不需要任何转义。
  • @RickJames 我认为嵌套引用不起作用。 db 函数根本不应该被引用。你说得对last_insert_id() 我并不精通SQL。谢谢你的小费。 :)

标签: mysql go spatial go-gorm


【解决方案1】:

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

根据这个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 says4326 是最常见的空间参考,用于存储全球范围内的参考数据。它作为 PostGIS 空间数据库和 GeoJSON 标准的默认值。在大多数 web 制图中也默认使用它图书馆。)

【讨论】:

  • 非常感谢您的详细解释。效果很好。没有你的帮助,我无法弄清楚。还有一个encoding/wekb,但值非常接近 0,我不知道为什么。我也尝试使用类型转换而不是封装,但是来自扫描接口的缓冲区(src)的长度似乎是错误的。但是用你的方法它是有效的。
  • 别担心,希望对您有所帮助。
【解决方案2】:

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

Hooks 可以让您在 Gorm 的 sql 生成之前将该列设置为 gorm.Expr

例如,在插入之前是这样的:

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
}

【讨论】:

  • 很好奇,在这种情况下,我的自定义扫描器是用参数 *gorm.SqlExpr 调用的。而且我不知道该怎么办。查询期间需要此扫描仪。将其注释掉也无助于创建数据。我收到了Error 1364: Field 'loc' doesn't have a default value
  • 我在添加你的钩子之前添加了我的跟踪结果。
  • field 'loc' doesn't have a default value 如果 db 中的列不为空,但未设置插入(例如,忽略/未导出),则发生,并且 Gorm 似乎需要模型上的默认值作为属性。在准备插入值时调用扫描,我不知道为什么,但假设这是为了仔细检查。虽然你可以调用 SetColumn 来设置一个 *SqlExpr,但是在插入的时候并没有用到它。如果字段的原始类型是 *SqlExpr,我认为会使用它。很抱歉,这种方法似乎行不通。另一个想法可能是创建自己的方言。
  • 马克你知道第 133 行的位置吗?我正在尝试查找 scope.SQLDB().Exec() 的定义。 callback_create.go
  • 我正试图找出那个额外的单引号是在哪里构造的。如果我能弄清楚,我希望我能定制它。这是方言的功劳吗?
猜你喜欢
  • 1970-01-01
  • 2013-05-17
  • 2017-03-14
  • 2015-08-21
  • 2019-11-09
  • 2010-11-12
  • 1970-01-01
  • 1970-01-01
  • 2015-08-18
相关资源
最近更新 更多