neo4j / neo4j-go-driver

Neo4j Bolt Driver for Go
Apache License 2.0
488 stars 68 forks source link

Introduce code generation-based record mapper #423

Closed fbiville closed 1 year ago

fbiville commented 1 year ago

Note, this is much "quick'n'dirtier" than https://github.com/neo4j/neo4j-go-driver/pull/420.

Pros (compared to the other PR)

Cons

fbiville commented 1 year ago

Example:

package users

// [...]

//go:generate go run github.com/neo4j/neo4j-go-driver/v5/cmd/mapper "Person"
type Person struct {
    Labels    []string       `json:"labello" neo4j:"mapping_type=labels"`
    ElementId string         `neo4j:"mapping_type=element_id"`
    Id        int64          `neo4j:"mapping_type=id" json:"id_please"`
    Name      string         `neo4j:"mapping_type=property,name=name"`
    Props     map[string]any `neo4j:"mapping_type=properties"`
    Ignored   bool           `json:"-"`
}

The generated code looks like:

// Code generated by neo4j-mapper-gen DO NOT EDIT

package users

import "github.com/neo4j/neo4j-go-driver/v5/neo4j"

func MapPersonFromRecord(record *neo4j.Record) (*Person, error) {
    value := record.Values[0]
    result := &Person{}
    result.Labels = value.(neo4j.Node).Labels
    result.ElementId = value.(neo4j.Entity).GetElementId()
    result.Id = value.(neo4j.Entity).GetId()
    result.Name = value.(neo4j.Entity).GetProperties()["name"].(string)
    result.Props = value.(neo4j.Entity).GetProperties()
    return result, nil
}

Error handling is completely skipped here since this PR is only a POC.

fbiville commented 1 year ago

Similar to #420, I ran a small benchmark on my machine against a local Neo4j instance (via Neo4j Desktop):

//go:generate go run github.com/neo4j/neo4j-go-driver/v5/cmd/mapper "APerson"
type APerson struct {
    Labels   []string `neo4j:"mapping_type=labels"`
    Id       int64    `neo4j:"mapping_type=id"`
    BetterId string   `neo4j:"mapping_type=element_id"`
    Name     string   `neo4j:"mapping_type=property,name=name"`
}

func BenchmarkMapSingle(b *testing.B) {
    ctx := context.Background()
    auth := neo4j.BasicAuth("neo4j", "admin", "")
    driver, err := neo4j.NewDriverWithContext("neo4j://localhost", auth)
    if err != nil {
        b.Fatal(err)
    }
    b.Cleanup(func() {
        if err := driver.Close(ctx); err != nil {
            b.Error(err)
        }
    })
    session := driver.NewSession(ctx, neo4j.SessionConfig{})
    b.Cleanup(func() {
        if err := session.Close(ctx); err != nil {
            b.Error(err)
        }
    })
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        result, err := session.Run(ctx, "MATCH (p:Person) RETURN p LIMIT 1", nil)
        if err != nil {
            b.Error(err)
            continue
        }
        record, err := result.Single(ctx)
        if err != nil {
            b.Error(err)
            continue
        }
        _, err = MapAPersonFromRecord(record)
        if err != nil {
            b.Error(err)
        }
    }
}

The generated code looks as follows:

// Code generated by neo4j-mapper-gen DO NOT EDIT

package neo4j_test

import "github.com/neo4j/neo4j-go-driver/v5/neo4j"

func MapAPersonFromRecord(record *neo4j.Record) (*APerson, error) {
    value := record.Values[0]
    result := &APerson{}
    result.Labels = value.(neo4j.Node).Labels
    result.Id = value.(neo4j.Entity).GetId()
    result.BetterId = value.(neo4j.Entity).GetElementId()
    result.Name = value.(neo4j.Entity).GetProperties()["name"].(string)
    return result, nil
}
goos: darwin
goarch: arm64
pkg: github.com/neo4j/neo4j-go-driver/v5/neo4j
BenchmarkMapSingle
BenchmarkMapSingle-10           4344        232505 ns/op
PASS