nebula graph orm框架 norm 用法解析 - 结构迁移、标签配置

norm 是一款专为 nebula graph 设计的 orm 框架,关于框架的基本用法可以参考此文章:norm 用法解析 - 基本用法

norm 原本的名称为 nebulaorm,实际使用起来 nebulaorm 这个名字确实还是太长了,一堆字母堆在一起也不太优雅,考虑到框架刚刚到0.3版本,并且目前用的人确实也不太多(目前就2个issue),我最终还是做了个非常大的不兼容更新,把整个项目的名字从 nebulaorm 换成了 norm,不过换了名字之后看起来顺眼多了,和结构体的tag名 norm 也保持了一致。目前整个项目的结构基本确定,最核心的功能也都有了,后面肯定不会再做这么大的不兼容更新了。

新特性:结构迁移(Migrate)

norm 最近主要是新支持了节点和边的结构迁移,即 migrate 操作,类似于 gorm 对关系型数据库进行表结构迁移,由于 nebula graph 也是强 schema 的,tag 和 edge 的结构必须预先定义好才能使用,故这个功能还是很有必要的。norm 暴露出的方法也和 gorm 很接近,以便于开发者使用。接下来我详细地介绍一下 migrate 的具体用法:

迁移节点中的tag结构:

示例代码如下:

go 复制代码
package main

import (
    "github.com/haysons/norm"
    "log"
    "time"
)

var db *norm.DB

func main() {
    // 初始化 norm.DB 实例
    log.SetFlags(log.LstdFlags | log.Lshortfile)
    conf := &norm.Config{
       Username:    "root",
       Password:    "nebula",
       SpaceName:   "test",
       Addresses:   []string{"127.0.0.1:9669"},
       ConnTimeout: 10 * time.Second,
    }
    var err error
    db, err = norm.Open(conf)
    if err != nil {
       log.Fatal(err)
    }
    defer db.Close()

    migrateTags()
}

// Woman 需要进行迁移的原始结构体
type Woman struct {
    VID     string `norm:"vertex_id"`
    Name    string `norm:"index:,length:5"`
    Age     int    `norm:"index:idx_woman_age_married,priority:1"`
    Married bool   `norm:"index:idx_woman_age_married,priority:2"`
    Salary  float64
}

func (t *Woman) VertexID() string {
    return t.VID
}

func (t *Woman) VertexTagName() string {
    return "woman"
}

// WomanUpdate 对 Woman 进行更新后的结构体
type WomanUpdate struct {
    VID     string `norm:"vertex_id"`
    Name    string `norm:"prop:name;not_null;default:''"`
    Age     int    `norm:"prop:age;not_null;default:0"`
    Married bool   `norm:"prop:married;not_null;default:false"`
}

func (t *WomanUpdate) VertexID() string {
    return t.VID
}

func (t *WomanUpdate) VertexTagName() string {
    return "woman"
}

func migrateTags() {
    // 开启 debug 模式便于观察执行的语句
    migrator := db.Debug().Migrator()

    // 判断 woman tag 是否存在
    hasTag, err := migrator.HasVertexTag("woman")
    if err != nil {
       log.Fatal(err)
    }
    log.Printf("has vertex tag: %v", hasTag)

    // 基于结构体自动创建tag
    if err = migrator.AutoMigrateVertexes(Woman{}); err != nil {
       log.Fatal(err)
    }

    // 查看tag创建后的各个属性
    womanProps, err := migrator.DescVertexTag("woman")
    if err != nil {
       log.Fatal(err)
    }
    for _, womanProp := range womanProps {
       log.Printf("woman prop: %+v\n", womanProp)
    }

    // 删除tag上的索引
    if err = migrator.DropVertexTagIndex("idx_woman_name", true); err != nil {
       log.Fatal(err)
    }
    if err = migrator.DropVertexTagIndex("idx_woman_age_married", true); err != nil {
       log.Fatal(err)
    }

    // 使用 WomanUpdate 更新 woman tag
    migrator = norm.NewMigrator(db.Debug())
    if err = migrator.AutoMigrateVertexes(WomanUpdate{}); err != nil {
       log.Fatal(err)
    }

    // 查看更新后的tag的各个属性
    womanProps, err = migrator.DescVertexTag("woman")
    if err != nil {
       log.Fatal(err)
    }
    for _, womanProp := range womanProps {
       log.Printf("woman prop: %+v\n", womanProp)
    }

    // 删除 woman tag
    if err = migrator.DropVertexTag("woman"); err != nil {
       log.Fatal(err)
    }
}

// output:

// has vertex tag: false

// woman prop: &{Field:name Type:string Null:YES Default:_EMPTY_ Comment:_EMPTY_}
// woman prop: &{Field:age Type:int64 Null:YES Default:_EMPTY_ Comment:_EMPTY_}
// woman prop: &{Field:married Type:bool Null:YES Default:_EMPTY_ Comment:_EMPTY_}
// woman prop: &{Field:salary Type:double Null:YES Default:_EMPTY_ Comment:_EMPTY_}

// woman prop: &{Field:name Type:string Null:NO Default: Comment:_EMPTY_}
// woman prop: &{Field:age Type:int64 Null:NO Default:0 Comment:_EMPTY_}
// woman prop: &{Field:married Type:bool Null:NO Default:false Comment:_EMPTY_}
// woman prop: &{Field:salary Type:double Null:YES Default:_EMPTY_ Comment:_EMPTY_}

首先需要调用 Migrator() 方法,创建 migrator 对象,这点和 gorm 是保持一致的,使用 HasVertexTag 方法可以判断节点的 tag 是否存在,从日志可以看到最开始 woman tag 是不存在的,之后通过 AutoMigrateVertexes 方法自动迁移了 Woman 结构体,这个方法可以传入多个节点, 将依次迁移每个节点包含的tag,若一个节点包含多个tag也可以正常完成迁移 ,如这里的 Woman 结构体:

go 复制代码
type Woman struct {
    VID     string `norm:"vertex_id"`
    Name    string `norm:"index:,length:5"`
    Age     int    `norm:"index:idx_woman_age_married,priority:1"`
    Married bool   `norm:"index:idx_woman_age_married,priority:2"`
    Salary  float64
}

结构体的各字段没有指定更多自定义的信息,故创建的tag各属性都会只声明属性名称及数据类型,其余如 "是否允许为null"、"字段的默认值" 等都将保持默认;同时,结构体还在Name字段上声明了一个单列索引,在Age及Married字段上声明了一个联合索引,声明索引的方式和 gorm 基本一致。

在字段的 norm tag 上添加 index 标记,则会添加一个对应属性的单列索引,若未指定索引名称,则默认的命名方式为 idx_tag_prop,比如这里就声明了一个 idx_woman_name 索引,同时 nebula graph 要求对变长字符串声明索引时必须指定索引长度,故这里指定 length:5,即只使用 name 属性的前5个字符来创建索引。

之后通过在多个字段上声明同一个索引名称来指定联合索引,比如这里的 idx_woman_age_married,通过 priority 指定了索引各字段的顺序,和 gorm 一致,priority 值默认为10,priority 越小,字段排在索引的越前面。

有一点需要说明的是,norm 只会自动创建不存在的索引,若同名索引已经存在,则不会进行创建也不会进行更新之类的操作,也不会删除已经存在的索引,另外在 nebual graph 中,只是执行创建索引的语句是不会实际索引数据的,还需要执行 rebuild index 语句,但 norm 并不会在执行完创建索引的语句后自动执行重建语句,因为索引的创建是异步的,创建完索引后需要等待一段时间才能执行重建语句,这个不确定的等待时间不太好加入到一个自动化的流程中,故norm 提供了 RebuildVertexTagIndexesRebuildEdgeIndexes方法,开发者需要自行完成索引的重建。

创建tag之后,使用 DescVertexTag 方法查看tag中包含的属性,可看到如下属性:

ruby 复制代码
woman prop: &{Field:name Type:string Null:YES Default:_EMPTY_ Comment:_EMPTY_}
woman prop: &{Field:age Type:int64 Null:YES Default:_EMPTY_ Comment:_EMPTY_}
woman prop: &{Field:married Type:bool Null:YES Default:_EMPTY_ Comment:_EMPTY_}
woman prop: &{Field:salary Type:double Null:YES Default:_EMPTY_ Comment:_EMPTY_}

可以看到属性名默认使用了字段名驼峰转下划线,属性的数据类型由结构体的字段类型映射得到,而 "属性是否允许为null"、"属性默认值"、"属性说明" 都保持了默认值。

以下是上述逻辑实际执行的 nGQL:

go 复制代码
// 判断tag是否存在时,使用了 SHOW TAGS 语句

result.go:27: [DEBUG] [norm] nGQL: SHOW TAGS

// 判断tag并不存在,则执行了创建tag的语句,可以看到语句中只声明了属性名称及数据类型,其他配置均保持了默认

result.go:38: [DEBUG] [norm] nGQL: CREATE TAG IF NOT EXISTS woman(name string, age int, married bool, salary double);

// 创建索引前先要判断索引是否存在,若已存在则不再创建,故这里先执行了 SHOW TAG INDEXES 语句判断索引是否存在

result.go:27: [DEBUG] [norm] nGQL: SHOW TAG INDEXES

// 创建name属性的单列索引,name属性的索引长度为5

result.go:38: [DEBUG] [norm] nGQL: CREATE TAG INDEX IF NOT EXISTS idx_woman_name ON woman(name(5));

// 创建age及married属性的多列索引,按照属性优先级的要求,age属性排在前面

result.go:38: [DEBUG] [norm] nGQL: CREATE TAG INDEX IF NOT EXISTS idx_woman_age_married ON woman(age, married);

// 查询tag的详细属性信息

result.go:27: [DEBUG] [norm] nGQL: DESCRIBE TAG woman

接下来将结构体的字段进行调整,tag也会同步进行更新,这是更新后的结构体,

go 复制代码
type WomanUpdate struct {
    VID     string `norm:"vertex_id"`
    Name    string `norm:"prop:name;not_null;default:''"`
    Age     int    `norm:"prop:age;not_null;default:0"`
    Married bool   `norm:"prop:married;not_null;default:false"`
}

可以看到属性都指定了 not null 以及默认值,在 nebula graph 中,如果想要对节点的属性进行调整,就需要先将属性关联的索引删除,故这里使用 DropVertexTagIndex 方法将上面新建的两个索引删除,之后使用 AutoMigrateVertexes 再次进行tag结构的迁移,可以看到迁移之后的属性如下:

ruby 复制代码
woman prop: &{Field:name Type:string Null:NO Default: Comment:_EMPTY_}
woman prop: &{Field:age Type:int64 Null:NO Default:0 Comment:_EMPTY_}
woman prop: &{Field:married Type:bool Null:NO Default:false Comment:_EMPTY_}
woman prop: &{Field:salary Type:double Null:YES Default:_EMPTY_ Comment:_EMPTY_}

name、age、married 属性都被声明为了 not null,同时都指定了默认值,salary 在新的结构体中被去掉了,但出于安全考虑 norm 不会删除旧的属性 ,这点和 gorm 也是一致的。

以下是上述逻辑实际执行的 nGQL:

go 复制代码
// 删除两个索引

result.go:38: [DEBUG] [norm] nGQL: DROP TAG INDEX IF EXISTS idx_woman_name;

result.go:38: [DEBUG] [norm] nGQL: DROP TAG INDEX IF EXISTS idx_woman_age_married;

// 执行 SHOW TAGS ,用于判断 tag 是否已经存在

result.go:27: [DEBUG] [norm] nGQL: SHOW TAGS

// tag已经存在,获取 tag woman 的详细属性,比对新的结构体和旧tag属性的变化

result.go:27: [DEBUG] [norm] nGQL: DESCRIBE TAG woman

// 执行 alter 语句,更新存在变化的属性

result.go:38: [DEBUG] [norm] nGQL: ALTER TAG woman CHANGE (name string NOT NULL DEFAULT "", age int NOT NULL DEFAULT 0, married bool NOT NULL DEFAULT false);

// 查看 woman 更新后的属性值

result.go:27: [DEBUG] [norm] nGQL: DESCRIBE TAG woman

// 测试结束,删除此tag
result.go:38: [DEBUG] [norm] nGQL: DROP TAG woman;

迁移边的结构:

示例代码如下:

go 复制代码
// Follow 需要进行迁移的边的结构体
type Follow struct {
    SrcID string `norm:"edge_src_id"`
    DstID string `norm:"edge_dst_id"`
    P1    int    `norm:"index"`
    P2    bool
}

func (e Follow) EdgeTypeName() string {
    return "follow"
}

// FollowUpdate 更新后的边的结构体
type FollowUpdate struct {
    SrcID string `norm:"edge_src_id"`
    DstID string `norm:"edge_dst_id"`
    P1    int    `norm:"prop:p1;not_null;default:0"`
    P2    bool   `norm:"prop:p2;not_null;default:false"`
    P3    string `norm:"prop:p3;not_null;default:''"`
}

func (e FollowUpdate) EdgeTypeName() string {
    return "follow"
}

func migrateEdges() {
    migrator := db.Debug().Migrator()

    // 判断边是否存在
    hasEdge, err := migrator.HasEdge("follow")
    if err != nil {
       log.Fatal(err)
    }
    log.Printf("has edge: %v", hasEdge)

    // 自动迁移边结构
    if err = migrator.AutoMigrateEdges(&Follow{}); err != nil {
       log.Fatal(err)
    }

    // 查看边创建后的各个属性
    props, err := migrator.DescEdge("follow")
    if err != nil {
       log.Fatal(err)
    }
    for _, prop := range props {
       log.Printf("edge prop: %+v\n", prop)
    }

    // 删除创建的索引
    if err = migrator.DropEdgeIndex("idx_follow_p1", true); err != nil {
       log.Fatal(err)
    }

    // 使用 FollowUpdate 更新边的结构
    migrator = norm.NewMigrator(db.Debug())
    if err = migrator.AutoMigrateEdges(&FollowUpdate{}); err != nil {
       log.Fatal(err)
    }

    // 查看更新后边的各个属性
    props, err = migrator.DescEdge("follow")
    if err != nil {
       log.Fatal(err)
    }
    for _, prop := range props {
       log.Printf("edge prop: %+v\n", prop)
    }

    // 测试结束,删除创建的边
    if err = migrator.DropEdge("follow"); err != nil {
       log.Fatal(err)
    }
}

// output:
// has edge: false

// edge prop: &{Field:p1 Type:int64 Null:YES Default:_EMPTY_ Comment:_EMPTY_}
// edge prop: &{Field:p2 Type:bool Null:YES Default:_EMPTY_ Comment:_EMPTY_}

// edge prop: &{Field:p1 Type:int64 Null:NO Default:0 Comment:_EMPTY_}
// edge prop: &{Field:p2 Type:bool Null:NO Default:false Comment:_EMPTY_}
// edge prop: &{Field:p3 Type:string Null:NO Default: Comment:_EMPTY_}

nebula graph 中边的属性和tag的属性是完全一致的,这里的代码也几乎一样,只是换成了边相关的方法,就不详细说明了,可以看下具体执行的语句:

go 复制代码
// 通过 SHOW EDGES 语句判断边是否存在
result.go:27: [DEBUG] [norm] nGQL: SHOW EDGES

// 边不存在时,执行创建边的语句
result.go:38: [DEBUG] [norm] nGQL: CREATE EDGE IF NOT EXISTS follow(p1 int, p2 bool);

// 判断索引是否存在
result.go:27: [DEBUG] [norm] nGQL: SHOW EDGE INDEXES

// 索引不存在,执行创建索引的语句
result.go:38: [DEBUG] [norm] nGQL: CREATE EDGE INDEX IF NOT EXISTS idx_follow_p1 ON follow(p1);

// 查看创建后的的边的属性
result.go:27: [DEBUG] [norm] nGQL: DESCRIBE EDGE follow

// 删除边上存在的索引
result.go:38: [DEBUG] [norm] nGQL: DROP EDGE INDEX IF EXISTS idx_follow_p1;

// 判断边是否存在
result.go:27: [DEBUG] [norm] nGQL: SHOW EDGES

// 边已存在,则获取边的全部属性,比对属性是否发生变化
result.go:27: [DEBUG] [norm] nGQL: DESCRIBE EDGE follow

// 属性发生变化,执行 alter 语句更新边的属性
result.go:38: [DEBUG] [norm] nGQL: ALTER EDGE follow ADD (p3 string NOT NULL DEFAULT ""), CHANGE (p1 int NOT NULL DEFAULT 0, p2 bool NOT NULL DEFAULT false);

// 获取更新后的边的属性详情
result.go:27: [DEBUG] [norm] nGQL: DESCRIBE EDGE follow

// 删除刚刚创建的边
result.go:38: [DEBUG] [norm] nGQL: DROP EDGE follow;

字段标签:

接下来打算详细地介绍一下结构体上可以添加哪些norm标签,这些标签不方便直接从方法的注释或者名称中看出来,全部可以添加的标签都在这里定义成了常量,后续可以查看进行参考。

1. col

将结构体字段映射为数据库返回值的某个字段,比如下面这段代码:

go 复制代码
// GO FROM "player100" OVER follow \
// YIELD src(edge) AS id | \
// GO FROM $-.id OVER serve \
// WHERE properties($^).age > 20 \
// YIELD properties($^).name AS Friend, properties($$).name AS Team;

type record4 struct {
   Friend string `norm:"col:Friend"`
   Team   string `norm:"col:Team"`
}
record4s := make([]record4, 0)
err = db.Go().
   From("player100").
   Over("follow").
   Yield("src(edge) AS id").Pipe().
   Go().
   From(clause.Expr{Str: "$-.id"}).
   Over("serve").
   Where("properties($^).age > ?", 20).
   Yield("properties($^).name AS Friend, properties($$).name AS Team").
   Find(&record4s)
if err != nil {
   log.Fatal(err)
}
log.Printf("records: %+v\n", record4s)

数据库返回的记录包含 FriendTeam 两个字段,record4 结构体的两个字段就需要声明 col 标签来接收这两个字段,如果字段导出但是没有主动声明 col,那么默认将按照驼峰转下划线进行映射,故假如这里返回的是 friendteam 两个小写的字段,结构体就不需要显式声明 col 配置了。注意 col 声明的是数据库返回记录的字段名,而不是节点或者边的某个属性名,数据库返回的记录的某个字段完全可以是一个节点,甚至是节点的列表,比如这种:

go 复制代码
type edgeVertex struct {
   ID string `norm:"col:id"`
   T  *Team  `norm:"col:t"`
}

type Team struct {
   VID  string `norm:"vertex_id"`
   Name string `norm:"prop:name"`
}

edgeVertexes := make([]*edgeVertex, 0)
err = db.Go().
   From("player1001").
   Over("serve").
   Yield("id($^) as id, $$ as t").
   Find(&edgeVertexes)
if err != nil {
   log.Fatalf("get edge vertex failed: %v", err)
}
for _, v := range edgeVertexes {
   log.Printf("id: %v, t: %+v", v.ID, v.T)
}

此处返回的记录就包含一个id字段以及一个节点字段,记录的字段声明使用 col,节点内属性的名称声明使用 prop,可以说 col 是在返回值更外层的。

2. vertex_id

将结构体字段映射为节点的id,这样norm就可以将数据库返回的节点id赋值给该字段,示例如下:

go 复制代码
type Player struct {
    VID  string `norm:"vertex_id"`
    Name string `norm:"prop:name"`
    Age  int    `norm:"prop:age"`
}

func (p Player) VertexID() string {
    return p.VID
}

func (p Player) VertexTagName() string {
    return "player"
}

player := new(Player)
err := db.Fetch("player", "player1001").
   Yield("vertex as v").
   FindCol("v", player)
if err != nil {
   log.Fatalf("fetch player failed: %v", err)
}
log.Printf("player: %+v", player)

这里会将数据库返回的节点id赋值给 VID 字段,不过并不是所有结构体都需要有 vertex_id 标签字段,因为有些节点的id可能是通过属性计算出来的,比如:

go 复制代码
type Player struct {
    Name string `norm:"prop:name"`
    Age  int    `norm:"prop:age"`
}

func (p Player) VertexID() string {
    // 使用 name 属性的 md5 作为节点id
    return md5(p.Name)
}

func (p Player) VertexTagName() string {
    return "player"
}

此时使用 name 属性的md5作为节点id,也就不需要将某个字段声明为 vertex_id 了,但是如果一个结构体代表一个节点,则这个结构体一定要实现 VertexID() stringVertexID() int64 方法,这是一个结构体被当做节点的标志。

3. edge_src_id, edge_dst_id

将结构体的字段映射为边的起始id以及目的id,使norm能够读取边结构体变量的起始id与目的id,同时也能将数据库返回的边的起始id和目的id赋值到该字段之上,示例如下:

go 复制代码
type Follow struct {
    SrcID  string `norm:"edge_src_id"`
    DstID  string `norm:"edge_dst_id"`
    Rank   int    `norm:"edge_rank"`
    Degree int64  `norm:"prop:degree"`
}

func (f Follow) EdgeTypeName() string {
    return "follow"
}

// LOOKUP ON follow WHERE follow.degree == 90 YIELD edge AS e;

edges := make([]Follow, 0)
err = db.Lookup("follow").
   Where("follow.degree == ?", 90).
   Yield("edge AS e").
   FindCol("e", &edges)
if err != nil {
   log.Fatal(err)
}
log.Printf("edges: %+v\n", edges)

作为边的结构体必须要实现 EdgeTypeName() string 方法,告知边的类型名称,同时也必须声明两个字段分别作为 edge_src_idedge_dst_id,否则解析结构体时会返回错误。

4. edge_rank

标记字段为边的 rank 值,此标记非必须,如果边存在 rank 值则可通过此标签声明,这样写入边的时候即可同时写入边的 rank 值,数据库返回边的 rank 值时赋值到此字段之上,示例如下:

go 复制代码
type E1 struct {
    SrcID string `norm:"edge_src_id"`
    DstID string `norm:"edge_dst_id"`
    Rank  int    `norm:"edge_rank"`
}

func (e *E1) EdgeTypeName() string {
    return "e1"
}

if err := db.InsertEdge(&E1{SrcID: "10", DstID: "11"}).Exec(); err != nil {
   log.Fatal(err)
}

5. prop

将结构体的字段映射为节点或边的属性,prop 可标记属性的名称,示例如下:

go 复制代码
type Follow struct {
    SrcID  string `norm:"edge_src_id"`
    DstID  string `norm:"edge_dst_id"`
    Rank   int    `norm:"edge_rank"`
    Degree int64  `norm:"prop:degree"`
}

func (f Follow) EdgeTypeName() string {
    return "follow"
}

// LOOKUP ON follow WHERE follow.degree == 90 YIELD edge AS e;

edges := make([]Follow, 0)
err = db.Lookup("follow").
   Where("follow.degree == ?", 90).
   Yield("edge AS e").
   FindCol("e", &edges)
if err != nil {
   log.Fatal(err)
}
log.Printf("edges: %+v\n", edges)

节点或边的结构体中的导出字段不加任何tag,也会被视作属性,此时属性名默认为字段名驼峰转下划线。

6. type

将结构体的字段映射为节点或边的属性时 type 指定了映射到 nebula graph 中的数据类型,故 type 的具体值只能是官方文档中规定的数据类型,这里的数据类型一方面是为了migrate 操作时指定建表语句中属性的数据类型,同时也便于 norm 生成 nGQL 语句,具体示例如下:

go 复制代码
type vm5 struct {
    VID        string    `norm:"vertex_id"`
    Name       string    `norm:"prop:name;type:fixed_string(32);default:hayson"`
    Age        int       `norm:"prop:age;type:int32;default:20"`
    CreateTime time.Time `norm:"default:datetime(1625469277)"`
}

func (t *vm5) VertexID() string {
    return t.VID
}

func (t *vm5) VertexTagName() string {
    return "woman"
}

若没有主动声明 type,此时数据类型会基于结构体字段go语言类型做一个默认的映射,比如go中的 float32 映射为nebula graph中的 float,go中的 float64 映射为nebula graph中的 double

7. not_null

声明节点或边的属性不允许为null,此配置主要是在 migrate 操作中生效,nebula graph 中属性默认为允许为null,添加此配置之后,则生成的建表语句中对应属性不允许为null。如:

go 复制代码
type WomanUpdate struct {
    VID     string `norm:"vertex_id"`
    Name    string `norm:"prop:name;not_null;default:''"`
    Age     int    `norm:"prop:age;not_null;default:0"`
    Married bool   `norm:"prop:married;not_null;default:false"`
}

func (t *WomanUpdate) VertexID() string {
    return t.VID
}

func (t *WomanUpdate) VertexTagName() string {
    return "woman"
}

这个结构体就声明了 name, age, married 属性是不允许为null的。

8. default

指定节点或边属性的默认值,此配置主要是在 migrate 操作中生效,添加此配置之后,则生成的结构迁移语句中的对应属性会加上指定的默认值,如:

go 复制代码
type WomanUpdate struct {
    VID     string `norm:"vertex_id"`
    Name    string `norm:"prop:name;not_null;default:''"`
    Age     int    `norm:"prop:age;not_null;default:0"`
    Married bool   `norm:"prop:married;not_null;default:false"`
}

func (t *WomanUpdate) VertexID() string {
    return t.VID
}

func (t *WomanUpdate) VertexTagName() string {
    return "woman"
}

这里 name 属性的默认值为 '',也就是空字符串,如果希望是其他的字符串,则直接声明字符串即可,比如 default:Tomage 属性默认值为0,married 属性默认值为false。

9. comment

指定节点或边属性的描述信息,此配置主要是在 migrate 操作中生效,添加此配置之后,则生成的结构迁移语句中的对应属性会加上字段的描述信息,如:

go 复制代码
type vm1 struct {
    VID  string `norm:"vertex_id"`
    Name string `norm:"comment:名称字段"`
}

这里生成的创建tag的语句中 name 属性的描述信息为 名称字段

10. ttl

指定节点或边的某个属性作为存活时间属性,一个 tag 或 edge 只能有一个 ttl 属性,当该属性到达 ttl 设定的值之后这条数据就会被自动删除,在结构体中声明此配置之后,迁移语句会将对应属性作为 ttl 属性,如:

go 复制代码
type em4 struct {
    SrcID string `norm:"edge_src_id"`
    DstID string `norm:"edge_dst_id"`
    P1    string `norm:"index:,length:5"`
    P2    int    `norm:"ttl:100;index:idx_p2"`
    P3    string `norm:"prop:p3;type:timestamp"`
}

func (e em4) EdgeTypeName() string {
    return "e1"
}

最终会生成这样的边创建语句:

sql 复制代码
CREATE EDGE IF NOT EXISTS e1(p1 string, p2 int, p3 timestamp) TTL_DURATION = 100, TTL_COL = "p2";`

这里就会将 p2 设置为 ttl 属性,并且对应的生存时间设置为了100。tag 或 edge 中只能有一个属性增加 ttl 标记,如果存在多个则生成迁移语句时会返回错误。

11. index

为节点或边的属性创建索引,可以创建单列索引,示例如下:

go 复制代码
type vm1 struct {
    VID  string `norm:"vertex_id"`
    Name string `norm:"index:,length:5"`
    Age  int    `norm:"index"`
}

func (t vm1) VertexID() string {
    return t.VID
}

func (t vm1) VertexTagName() string {
    return "player"
}

这里会为两个属性分别创建单列索引:

sql 复制代码
CREATE TAG INDEX IF NOT EXISTS idx_player_name ON player(name(5));
CREATE TAG INDEX IF NOT EXISTS idx_player_age ON player(age);

由于并没有显式指定索引的名称,所以默认会使用 idx_tag名称_属性名称idx_边名称_属性名称 作为索引的名称。 如果加上索引名称则这里可以这样写:

go 复制代码
type vm1 struct {
    VID  string `norm:"vertex_id"`
    Name string `norm:"index:idx_player_name,length:5"`
    Age  int    `norm:"index:idx_player_age"`
}

最终生成的创建tag的语句也是一样的。同时针对于数据类型为变长字符串的属性,创建索引时必须指定索引长度,故这里多加了一个 length:5 的配置。

接下来可以看一个联合索引:

go 复制代码
type vm3 struct {
    VID  string `norm:"vertex_id"`
    Name string `norm:"prop:name;type:string;default:'';index:idx_name_age,length:5"`
    Age  int    `norm:"prop:age;type:int;default:20;index:idx_name_age"`
}

生成的语句如下:

sql 复制代码
CREATE TAG INDEX idx_name_age ON player_with_default(name(5), age);

若多个字段声明了同一个索引名称,如这里的 idx_name_age,这些字段就会构建为一个联合索引,索引中各属性的顺序默认会和结构体字段的顺序保持一致,如果希望自定义索引的顺序可以使用 priority 配置项,示例如下:

go 复制代码
type vm4 struct {
    VID        string    `norm:"vertex_id"`
    Name       string    `norm:"index:,length:5"`
    Age        int       `norm:"index:i_age"`
    Married    bool      `norm:"index:idx_married_salary"`
    Salary     float64   `norm:"index:idx_married_salary,priority:1"`
    CreateTime time.Time `norm:"type:timestamp;ttl:100"`
}

这里会创建3个索引:

sql 复制代码
CREATE TAG INDEX IF NOT EXISTS idx_woman_name ON woman(name(5));
CREATE TAG INDEX IF NOT EXISTS i_age ON woman(age);
CREATE TAG INDEX IF NOT EXISTS idx_married_salary ON woman(salary, married);

对于联合索引 idx_married_salarySalary 字段声明了 priority:1,而 Married 字段没有声明,默认为10,priority 值越小,属性在索引中会排在越前面,故这里索引创建语句中 salary 排在前面,这里和 gorm 是基本一致的。

12. -

-norm 的作用和 gorm 是一致的,让框架主动忽略结构体中的字段,此外如果结构体的字段未导出,norm 也会忽略该字段。

更多关于 norm 的详细用法,可以参考此目录:example,这里几乎把各种语句的用法都列举了一遍,应该足够日常开发使用了。

相关推荐
outsider_友人A3 天前
前端也想写后端(2)ORM框架以及数据库关系和操作
node.js·orm·nestjs
Code季风4 天前
GORM 一对多关联(Has Many)详解:从基础概念到实战应用
数据库·go·orm
白应穷奇7 天前
Diesel异步编程: 深入理解Rust ORM的异步特性
rust·orm
xjm爱学习7 天前
最强ORM让你开发效率提升百倍
java·后端·orm
薛家明8 天前
最强ORM让你开发效率提升百倍
java·orm·easy-query
濮水大叔8 天前
如何基于动态关系进行ORM关联查询,并动态推断DTO?
typescript·node.js·orm
濮水大叔10 天前
Prisma不能优雅的支持DTO,试试Vona ORM吧
前端框架·node.js·orm
Kookoos14 天前
ABP VNext + GraphQL Federation:跨微服务联合 Schema 分层
后端·微服务·.net·graphql·abp vnext·schema 分层