Вставка и выбор PostGIS Geometry с помощью Gorm

Я пытался найти способ вставлять и извлекать геометрические типы, используя Golang, и в частности библиотечный gorm. Я также пытаюсь использовать библиотечный шар, который определяет различные типы для геометрий и обеспечивает кодирование / декодирование между различными форматами.

Сфера имеет Scan() а также Value() методы уже реализованы для каждого типа. Это позволяет идти Insert() а также Scan() функции для работы с типами, отличными от примитивов. Однако ожидается, что Orb будет использовать геометрию, представленную в хорошо известном двоичном (WKB) формате.

Документация Orb показывает, что для этого вам нужно просто обернуть поле в функции PostGIS ST_AsBinary() а также ST_GeomFromWKB() для запросов и вставки соответственно. Например, с таблицей, определенной как:

_, err = db.Exec(`
        CREATE TABLE IF NOT EXISTS orbtest (
            id SERIAL PRIMARY KEY,
            name TEXT NOT NULL,
            geom geometry(POLYGON, 4326) NOT NULL
        );
    `)

Вы можете просто сделать:

rows, err := db.Query("SELECT id, name, ST_AsBinary(geom) FROM orbtest LIMIT 1")

И для вставки (где p это orb.Point):

db.Exec("INSERT INTO orbtest (id, name, geom) VALUES ($1, $2, ST_GeomFromWKB($3))", 1, "Test", wkb.Value(p))

Вот моя проблема: используя GORM, я не могу позволить себе построить эти запросы с помощью этих функций. GORM автоматически вставит значения в базу данных с заданной структурой и просканирует данные во всей иерархии структуры. Те Scan() а также Value() методы вызываются за кулисами, без моего контроля.

Попытка напрямую вставить двоичные данные в столбец геометрии не будет работать, а прямой запрос столбца геометрии даст результат в шестнадцатеричном виде.

Я пробовал несколько подходов к базе данных, чтобы решить эту проблему. Я попытался создать виды, которые автоматически вызывают необходимые функции в столбцах геометрии. Это сработало для запросов, но не для вставки.

Можно ли создать какой-то триггер или правило, которое бы автоматически вызывало необходимые функции для входящих / исходящих данных?

Следует также отметить, что библиотека, над которой я работаю, работает полностью независимо от данных и схем, поэтому я не могу позволить себе роскошь жестко кодировать какие-либо запросы. Конечно, я мог бы написать функцию, которая сканирует всю модель данных и генерирует запросы с нуля, но я бы предпочел, если бы был лучший вариант.

Кто-нибудь знает способ заставить это работать в SQL? Возможность вызывать функции для столбца автоматически, просто запрашивая сам столбец?

Любой совет будет принята с благодарностью.

6 ответов

Решение

Решение, которое я в итоге использовал, было следующим:

Сначала я создал новые типы, которые обернули все типы сфер, например:

type Polygon4326 orb.Polygon
type Point4326 orb.Point

Затем я реализовал Scan(), Value() методы на каждый тип. Однако мне пришлось редактировать байты и конвертировать в / из шестнадцатеричного. Когда вы напрямую запрашиваете пространственный столбец в PostGIS, он возвращает шестнадцатеричное представление EWKB, по существу WKB, но включает 4 байта для представления идентификатора проекции (в моем случае 4326).

Перед вставкой мне пришлось добавить байты, которые представляют проекцию 4326.

Перед чтением мне пришлось удалить эти байты, поскольку встроенный в Orb сканер ожидал формат WKB.

Я использовал ответ @robbieperry22 с другой библиотекой кодирования и обнаружил, что мне вообще не нужно возиться с байтами.

Включено содержание для справки.

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"`
}

А затем применил небольшую настройку в части настройки / переноса таблицы:

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()

Можно ли создать какой-то триггер или правило, которое бы автоматически вызывало необходимые функции для входящих / исходящих данных?

Когда-либо пробовал Gorm Hooks, пример:

type Example struct {
    ID   int
    Name string
    Geom ...
}

func (e *Example) AfterFind() (err error) {
    e.Geom = ... // Do whatever you like here
    return
}

Есть несколько крючков, которые вы можете использовать. Я нахожу их довольно аккуратными и полезными.

Другое решение, которое я в итоге использовал, было с go-geos, поскольку я обнаружил, что мне нужно использовать библиотеку GEOS C. Благодаря этому я могу преобразовать структуру вWKT для вставки (поскольку postgis принимает его как обычный текст) и конвертировать из WKB при сканировании.

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
}

Это решение может быть не идеальным для всех, поскольку не всем нужно использовать библиотеку GEOS C, что может быть проблемой при работе с окнами. Я уверен, что то же самое можно сделать с помощью разных библиотек.

Я дополнительно реализовал UnmarshalJSON() а также MarshalJSON()в структуре, чтобы она могла автоматически маршалировать / демаршалировать GeoJSON, а затем беспрепятственно сохранять / получать из базы данных. Я выполнил это с помощью geojson-go для преобразования GeoJSON в / из структуры, а затем geojson-geos-go для преобразования указанной структуры в структуру go-geos, которую я использовал. Немного запутанно, да, но это работает.

Вы можете сделать это следующим образом:

      package utils

import (
    "context"
    "encoding/json"
    "fmt"

    geojson "github.com/paulmach/go.geojson"
    "gorm.io/gorm"
    "gorm.io/gorm/clause"
)

type GeoPoint geojson.Geometry

func (g GeoPoint) GormDataType() string {
    return "geography(Point, 4326)"
}

func (g GeoPoint) GormDBDataType() string {
    return "geometry(Point, 4326)"
}
func (g GeoPoint) GormValue(ctx context.Context, db *gorm.DB) clause.Expr {
    if len(g.Point) == 0 { // Sprawdzanie, czy punkt jest pusty
        return clause.Expr{
            SQL: "NULL",
        }
    }

    geoJSONBytes, err := json.Marshal(g)
    if err != nil {
        return clause.Expr{SQL: "NULL"} // Obsłuż błąd
    }

    return clause.Expr{
        SQL:  "ST_SetSRID(ST_GeomFromGeoJSON(?),4326)",
        Vars: []interface{}{string(geoJSONBytes)},
    }
}

func (g *GeoPoint) Scan(input interface{}) error {
    switch value := input.(type) {
    case []byte:
        // Stworzenie pustego obiektu GeoPoint
        *g = GeoPoint{}
    case string:
        geom, err := geojson.UnmarshalGeometry([]byte(value))
        if err != nil {
            return fmt.Errorf("can't unmarshal GeoJSON: %w", err)
        }

        if geom.Type != geojson.GeometryPoint {
            return fmt.Errorf("expected point geometry, got %s", geom.Type)
        }

        // Przypisanie wartości do *g
        *g = GeoPoint(*geom)
    default:
        return fmt.Errorf("can't convert %T to GeoJSON", value)
    }

    return nil
}

И в описании модели:

          Position     utils.GeoPoint `gorm:"column:position"`

Далее, чтобы установить данные — это будет работать следующим образом (*s.Geometry из пакета geojson):

          utils.GeoPoint(*s.Geometry)

И для получения данных:

          result := store.db.Raw(`SELECT id, uuid, ST_AsGeoJSON(position) as position FROM table WHERE deleted_at IS NULL AND something_id = ?`, somethingID).Scan(&models)

(Я не нашел способа автоматизировать функцию ST_AsGeoJSON для определенного поля через GORM)

для gomigrate/v2, немного более новая обновленная версия кода выше, это то, что я использую:

      func customMigrateTables() error {

sqlStatements := []string{
    `CREATE SCHEMA IF NOT EXISTS "your custom schema"`,
    `CREATE EXTENSION IF NOT EXISTS postgis`,
    `CREATE TABLE IF NOT EXISTS "your custom table" (
            id SERIAL PRIMARY KEY,
            geom geometry(GEOMETRY, 4326) NOT NULL
        );`,
    // needed for some postgres id issue with gorm.
    `ALTER TABLE IF EXISTS dook.findings ALTER COLUMN "id" TYPE bigint`,
}

for _, stm := range sqlStatements {
    err := DB.Exec(stm).Error
    if err != nil {
        log.Fatal(err)
    }

}

err := gormigrate.New(DB, gormigrate.DefaultOptions, []*gormigrate.Migration{
    {
        ID: "init",
        Migrate: func(tx *gorm.DB) error {
            // your normal tables to be migrated.
            return tx.AutoMigrate(&Note{})
        },
    },
    {
        ID: "findings_except_geom",
        Migrate: func(tx *gorm.DB) error {
            return tx.AutoMigrate(Finding{})
        },
    },
}).Migrate()

return err

}

Другие вопросы по тегам