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
提供了 RebuildVertexTagIndexes
和 RebuildEdgeIndexes
方法,开发者需要自行完成索引的重建。
创建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)
数据库返回的记录包含 Friend
及 Team
两个字段,record4
结构体的两个字段就需要声明 col
标签来接收这两个字段,如果字段导出但是没有主动声明 col
,那么默认将按照驼峰转下划线进行映射,故假如这里返回的是 friend
和 team
两个小写的字段,结构体就不需要显式声明 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() string
或 VertexID() 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_id
与 edge_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:Tom
,age
属性默认值为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_salary
在 Salary
字段声明了 priority:1
,而 Married
字段没有声明,默认为10,priority
值越小,属性在索引中会排在越前面,故这里索引创建语句中 salary
排在前面,这里和 gorm
是基本一致的。
12. -
-
在 norm
的作用和 gorm
是一致的,让框架主动忽略结构体中的字段,此外如果结构体的字段未导出,norm
也会忽略该字段。
更多关于 norm
的详细用法,可以参考此目录:example,这里几乎把各种语句的用法都列举了一遍,应该足够日常开发使用了。