I\'ve been trying to find a way to insert and retrieve geometric types using Golang, and specifically the library gorm. I\'m also attempting to use the library orb that defines
The solution I ended up using was as follows:
First I created new types that wrapped all of the orb types, for example:
type Polygon4326 orb.Polygon
type Point4326 orb.Point
Then I implemented the Scan()
, Value()
methods on each type. I had to however edit the bytes and convert to/from hexadecimal. When you directly query on a spatial column in PostGIS, it will return a hexadecimal representation of EWKB, essentially WKB, but including 4 bytes to represent the projection ID (in my case 4326).
Before inserting, I had to add the bytes that represent the projection of 4326.
Before reading, I had to strip those bytes, since orb's built in scanning expected WKB format.
I used @robbieperry22's answer with a different encoding library and found I didn't need to tinker with bytes at all.
Included gist for reference.
import "github.com/twpayne/go-geom/encoding/geojson"
type EWKBGeomPoint geom.Point
func (g *EWKBGeomPoint) Scan(input interface{}) error {
gt, err := ewkb.Unmarshal(input.([]byte))
if err != nil {
return err
}
g = gt.(*EWKBGeomPoint)
return nil
}
func (g EWKBGeomPoint) Value() (driver.Value, error) {
b := geom.Point(g)
bp := &b
ewkbPt := ewkb.Point{Point: bp.SetSRID(4326)}
return ewkbPt.Value()
}
type Track struct {
gorm.Model
GeometryPoint EWKBGeomPoint `gorm:"column:geom"`
}
And then used a little customization on the table set up/migrate part:
err = db.Exec(`CREATE TABLE IF NOT EXISTS tracks (
id SERIAL PRIMARY KEY,
geom geometry(POINT, 4326) NOT NULL
);`).Error
if err != nil {
return err
}
err = gormigrate.New(db, gormigrate.DefaultOptions, []*gormigrate.Migration{
{
ID: "init",
Migrate: func(tx *gorm.DB) error {
return tx.CreateTable(
Tables...,
).Error
},
},
{
ID: "tracks_except_geom",
Migrate: func(tx *gorm.DB) error {
return db.AutoMigrate(Track{}).Error
},
},
}).Migrate()
Is it possible to make some sort of trigger or rule that would automatically call the needed functions on the data coming in/out?
Ever tried gorm hooks, example:
type Example struct {
ID int
Name string
Geom ...
}
func (e *Example) AfterFind() (err error) {
e.Geom = ... // Do whatever you like here
return
}
There is a handful of hooks that you can use. I find them pretty neat and useful.
Another solution, which I ended up using was with go-geos, as I discovered I needed to use the GEOS C library. With that, I am able to convert the struct into WKT
for inserting (as postgis accepts it as regular text) and convert from WKB
when scanning.
type Geometry4326 *geos.Geometry
// Value converts the given Geometry4326 struct into WKT such that it can be stored in a
// database. Implements Valuer interface for use with database operations.
func (g Geometry4326) Value() (driver.Value, error) {
str, err := g.ToWKT()
if err != nil {
return nil, err
}
return "SRID=4326;" + str, nil
}
// Scan converts the hexadecimal representation of geometry into the given Geometry4326
// struct. Implements Scanner interface for use with database operations.
func (g *Geometry4326) Scan(value interface{}) error {
bytes, ok := value.([]byte)
if !ok {
return errors.New("cannot convert database value to geometry")
}
str := string(bytes)
geom, err := geos.FromHex(str)
if err != nil {
return errors.Wrap(err, "cannot get geometry from hex")
}
geometry := Geometry4326(geom)
*g = geometry
return nil
}
This solution might not be ideal for everyone as not everyone needs to use the GEOS C library, which can be a pain to get working on windows. I'm sure though that the same thing can be accomplished using different libraries.
I additionally implemented UnmarshalJSON()
and MarshalJSON()
on the struct so that it can automatically Marshal/Unmarshal GeoJSON, and then save/get from the database seamlessly. I accomplished this using geojson-go to convert GeoJSON to/from a struct, and then geojson-geos-go to convert said struct into the go-geos struct I was using. A little convoluted, yes, but it works.