cayley原理2

cayley查询过程

cayley的查询过程相当复杂 下面是我个人的学习结果 仅仅是个人看法 无法保证绝对正确
微信图片_20180820172126.png-16.9kB


构造态射数组

在cayley中的查询一般我们都是构造一个path结构体实例 例如查询从A出发 经过节点12 可以达到什么节点

p := cayley.StartPath(store, quad.String("A")).Out(quad.Int(12))

上面的语句实质上返回一个path实例 就是p 对于我们用来查询的结构path

1
2
3
4
5
type Path struct {
stack []morphism
qs graph.QuadStore // Optionally. A nil qs is equivalent to a morphism.
baseContext pathContext
}

上面有一个重要的字段

stack       []morphism

morphism态射在范畴论中就是一个函数变换的抽象过程 cayley中的态射我们可以理解是我们每一个out int has等等指向下一个节点迭代的函数封装 我们每一个指向下一个的迭代(in out等)都转换为一个morphism态射 然后存在path中的stack态射数组

这个stack存储着表示我们查询的每一个划分(out in has等等)的态射

每一个态射在cayley中的定义

1
2
3
4
5
6
type morphism struct {
IsTag bool
Reversal func(*pathContext) (morphism, *pathContext)
Apply applyMorphism
tags []string
}

  • 其中Reversal返回的态射是一个反向的态射(比如out的反向态射就是in) 这就是cayley实现无向图的原因 每个态射都包含一个它的反向态射
  • Apply为执行这个态射的具体函数 这个函数返回值中得到的shape中就表示了执行态射后得到的节点

举一个简答的例子 也是上面那个例子

p := cayley.StartPath(store, quad.String("A")).Out(quad.Int(12))

上面这条查询语句在声明了一个path结构体的同时 也构造出我们上面所说的态射数组
下面的绿色大框即为我们的态射数组stack 两个蓝色框即为我们数组中的两个态射 都是morphism结构体 但是我们可以看红色框框 他们的apply这个函数是不一样的 一个是isMorphism 一个outMorphism

微信图片_20180820155549.png-45.6kB

我们可以很容易理解为什么有outMorphism态射 但是不能理解为什么有isMorphism这个态射 其实isMorphism是初始化的态射


构造shape

shape顾名思义就是我们查询路径的一种表现形式 表示了我们使用path查询始末的一条抽象路径 如上面的查询抽象的一个shape就是

A -> 12 ->

shape在cayley中只是一个接口 但凡实现了这个接口中的方法的结构体都是shape 具体的形式非常多样和复杂 我们这里不做详细研究 shape接口定义如下

1
2
3
4
5
6
7
8
9
10
11
12
// Shape represent a query tree shape.
type Shape interface {
// BuildIterator constructs an iterator tree from a given shapes and binds it to QuadStore.
BuildIterator(qs graph.QuadStore) graph.Iterator
// Optimize runs an optimization pass over a query shape.
//
// It returns a bool that indicates if shape was replaced and should always return a copy of shape in this case.
// In case no optimizations were made, it returns the same unmodified shape.
//
// If Optimizer is specified, it will be used instead of default optimizations.
Optimize(r Optimizer) (Shape, bool)
}

一个具体的shape的例子 可见shape的具体类型和机构是很多样的 没必要具体研究
微信图片_20180820162647.png-31.5kB

可以看到下面 随着态射一个一个被执行 我们的到的构造出来的shape也是变化的 每一次态射之后的到的shape都作为参数传入下一个被执行的态射函数 shape不断积累 不断拓张 最终形成我们需要的查询路径
微信图片_20180820162145.png-17.3kB

构造shape部分还包含了大量的shape优化 这部分复杂而内容很多 这里不展开


构造迭代器和迭代链

Iterator迭代器这个概念我们可以理解只负责某一部分计算工作的组件 cayley的查询设计是由若干个迭代器嵌套或者依次排列组合成的 呈现一个分布式结构 组合成的最终迭代器叫做IterateChain迭代器链 跟上面的shape一样 迭代的种类也是多种多样的 在这个分布式结构中的迭代器种类也不是都是类型一样的 所以宏观上我们不必关系具体使用了何种迭代器

cayley中对于迭代器的声明也是一个接口 在cayley中但凡实现了这个接口中所有方法的结构体我们都统称为迭代器Iterator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type Iterator interface {
String() string
Tagger() *Tagger
TagResults(map[string]Value)
Result() Value
Next(ctx context.Context) bool
NextPath(ctx context.Context) bool
Contains(ctx context.Context, v Value) bool
Err() error
Reset()
Clone() Iterator
Stats() IteratorStats
Size() (int64, bool)
Type() Type
Optimize() (Iterator, bool)
SubIterators() []Iterator
Close() error
UID() uint64
}

上面最重要的迭代器方法就是

  • Result() Value
  • Next(ctx context.Context) bool

Next方法被这个迭代器用来执行内嵌在它自己内部的另一个迭代器 Result这个函数用来获取执行结果

那么我们会很好奇迭代器这么抽象是怎样最终转化为mongodb的查询语句的呢?因为在cayley中使用的mongodb连接第三方包就是mgo.v2

其实我们知道迭代是一层一层嵌套的 那么我们可以看一下最底层的迭代器的结构 最底层的迭代器如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type Iterator struct {
uid uint64
tags graph.Tagger
qs *QuadStore
collection string
limit int64
constraint []FieldFilter
links []Linkage // used in Contains

iter DocIterator
result graph.Value
size int64
err error
}

这个迭代是位于cayley源代码中的nosql\iterator.go中声明的迭代器 其中我们可以看到它包含一个字段

constraint []FieldFilter

FieldFilter在cayley定义如下

1
2
3
4
5
6
// FieldFilter represents a single field comparison operation.
type FieldFilter struct {
Path []string // path is a path to specific field in the document
Filter FilterOp // comparison operation
Value Value // value that will be compared with field of the document
}

Filter FilterO是一个用来表示比较的字段 有

  • Equal
  • NotEqual
  • GT
  • GTE
  • LT
  • LTE

Value Value就是存储的节点值

语义上 上面这个结构体可以根据比较大小的操作符FilterOp和它的值value 来转化为在mongodb中对图的具体某个节点的查询

cayley原理一(获取查询结果)

查询结果的获取

首先我们假定cayley已将我们的查询语句path转化为mongodb的查询语句 现在我们分析cayley是如何从mongodb里面获取查询结果 并且返回给上层的

总体的结构如下(只包含从mongodb中获取查询结果 不包含解析为mongodb查询语句)
get.jpg-67.1kB

由cayley官方提供的hello world的查询方式如下

1
2
3
4
5
if err := p.Iterate(nil).EachValue(nil, func(value quad.Value) {
// ignore
}); err != nil {
panic(err)
}

可以看见是调用了EachValue这个函数 这个函数是属于一个叫做IterateChain结构体的一个方法 如上提最左边的类 上图最左边是我们的顶层调用 越往右是底层调用

为了直观 我们从右边到左边 从底层到高层的调用如下

1.mongo.go中的Iterator结构体(称为迭代器1)中调用Next()函数 这个Next()再调用mgo.v2\session.go中Iter结构体中的Next()函数 从而将从mgo中迭代的结果保存在迭代器1中的res字段中 这个字段类型是bson.M(实质上是一个map[string]interface{}类型) 然后迭代器1调用Doc()方法 将res类型转为Document(实质上为map[string]graph.Value)
image_1ckun71jb1l0s69khljmep15h2s.png-34.4kB

2.nosql\iterate.go中Iterator结构体(称为迭代器2)调用自己的Next()函数 进而调用步骤1中的迭代器1的Next()和Doc()函数完成步骤1的操作 并且转化为Document的结果存进迭代器2中的result字段中 类型是graph.Value
image_1ckun822b3bc14dec3k1u2pnvv39.png-60.3kB

3.iterate.go中的IterateChain结构体(称为迭代链)调用自己的Each()方法 进而 调用步骤2中的迭代器2中的Next()函数完成步骤2中的操作 并且使用迭代器2的Result()函数获取这个保存在迭代器2的Result字段中的结果
image_1ckup1a0kv53hqeo1s1lb51c2ds.png-50.9kB


使用接口实现多种实现方案

上面图中的两部分绿色非常关键 两部分都是定义了很多方法的接口 我们称最右边的接口为接口1 另一个称为接口2

对于接口1来说
这些包含了若干方法的接口是声明在nosql.go中的DocIterator这个接口中的 并且被nosql\iterate.go中进行调用 在接口的具体实现上我们上面的方案是使用了mongo.go文件中的Iterator结构体实现方式

试想一下:当我们实现另一个新的Iterator迭代器 并且都实现接口1的方法 但是在这些方法的定义上使用其他的nosql数据库进行底层存储 那么我就可以添加一种新的底层存储可选方案了

其实cayley可以使用mysql 还是mongodb bolt redis甚至内存等多种存储方式作为底层存储的原因就是:

其实cayley在sql还是nosql类型方案的选择上就是通过接口2的实现方式不一样来实现的 而nosql方式的底层存储数据库到底选择何种nosql数据库也是通过接口1的实现方式不一样来实现的 sql具体数据库的选择也是类似

图数据库跟传统数据库以及图算法

为什么需要图数据库

  • SQL依然是表哥数据的最好存储方式
  • s键值key-value依然是拓展性和速度上最快的
  • 图数据库只是为我们提供了查询图状数据结构的一种方式

为什么图状的数据结构的查询会对我们那么有用呢?
因为可以使用图状数据结构来表示人类可以直观理解的个体与个体之间的联系

但是为什么都使用其他的数据存储方式呢?

图数据库可以给你带来几样关键的好处

  1. 可以更简单地表示更复杂的数据
  2. 可以更灵活地改变你要存储的关系 SQL都是需要预先定义模式的 所以当关系不断变化的时候 SQL相比图数据库的劣势就出来了
  3. 可以更易于自定义关系 两个节点之间的关系想怎么定义就怎么定义
    4.可以支持一些设计图计算的算法 例如克鲁斯卡尔还有计算机网络的路由选路上涉及的一些算法

后面好像就没讲什么了 都是讲一些cayley图数据库的一些基础跟查询相关的 没有我想要的一些设计原理和并发处理的一些内容


戴克斯克拉算法

全局地寻找一个点到每个点的最短路径
此处输入图片的描述

距离向量算法

表示:
dx(y)x到y整条路径的最小cost
此处输入图片的描述
x到y的路径的cost等于它先到邻居的cost加上邻居到y的最小cost
此处输入图片的描述

Bellman-Ford例子:
此处输入图片的描述
要点:
Dx(y)是x到y的最小cost估计,N中每个节点都需要估计自己到y节点的最小cost。

每个节点中有一个距离向量
Dx = [Dx(y): y є N ]
这个距离向量包含着这个节点到N中所有节点最小cost的估计

而对于每个节点的邻居节点,也是这样:
Dv = [Dv(y): y є N ]

在距离向量算法下,每个节点需要维护的信息有:

  • 这个节点到邻居节点的cost
  • 这个节点到N中每个节点的预计最小cost,也就是距离向量
  • 这个节点邻居的距离向量

算法:

  • 每段时间按之后,每个节点把自己的距离向量副本发送给自己的邻居
  • 当每个节点接收到邻居发送过来的距离向量,拿这个距离向量更新自己的DV,使用BF公式

DV算法例子: 注意每个节点定时需要更新和把它的距离向量广播给它的邻居
此处输入图片的描述

LS和DV比较

  • LS需要知道达到所有节点的cost,因此需要洪泛,但是DV不需要
  • LS的时间复杂度是O(N^2),时间复杂度比DV更高

最小生成树

最小生成树就是给一个图给你 每个点都是可以互相联通的 但是每条边的权重是不一样的 需要找出一堆边 使得这些点都连接起来 但是都不构成环 而且这些权重架起来还是最小

image_1ckf7ba5is713q9122mg2v1i2j1t.png-58.1kB

最小生成树一个很常见的应用场景是 城市之间需要铺电缆 每个城市之间都是可以铺的 但是不同城市之间铺设电缆的成本不一样 那么就需要我们找出一个满足要求的最小生成树 来让成本最低

找最小生成树的方法 一般有Kruskal克鲁斯卡尔算法

步骤

  1. 新建图G,G中拥有原图中相同的节点,但没有边
  2. 将原图中所有的边按权值从小到大排序
  3. 从权值最小的边开始,如果这条边连接的两个节点于图G中不在同一个连通分量中,则添加这条边到图G中
  4. 重复3,直至图G中所有的节点都在同一个连通分量中

维基百科上给的例子 非常容易理解
image_1ckf7k3dbke01d0di4017nm1r0j2a.png-158.7kB

就是先找出一个图的所有的边 按照权重从小到大排序 一次选取权重最小的边 添加到结果图中 注意会跟已经选到的边构成环的边不要选择 抛弃就可以 知道图中的所有节点都在一个连通分量中


上面两种图的算法

其实上面我们要搞清楚
寻找最小生成树的算法

克鲁斯卡尔算法是寻找整个图的边权重之和最小的一种方法 寻找最小生成树

但是上面的两种算法 1.戴克斯克拉算法 2.距离向量算法 这两个算法都是路由选路算法

路由选路算法最终找到的结果并不是最小生成树 而是一个点到其它点距离最小的路径集合 注意这个不一定是最小生成树

使用rabbitMQ作为缓存队列

高并发请求情况

我们在服务端开发过程中 往往会遇到需要处理高并发请求 并且将post过来的数据存进数据库的情况 这种情况一般会对我们的两部分造成很大压力

  1. nginx等服务器
  2. 数据库连接

因为当并发量很大的时候 nginx等服务器会因为负载均衡受不了那么大的压力会崩掉 一般这样的话

用Unix socket或者tcp socket的方式增多几个web app实例以供nginx进行轮训 或者配置一下nginx的最大连接数 这样一般可以有效解决nginx服务器的问题

但是数据库的操作这边是无论如何也是需要一点时间的 如果并发量很大 那么久比较难解决 一般来说mongodb是可以通过在公有的连接池里面拷贝出一个新的session 但是这样的效果也是有限的 而比较有效的方法是

使用消息队列暂时存储来不及处理的数据 等服务端缓过来之后再冲消息队列里面获取数据 进行数据库操作


rabbitMQ

rabbitMQ就是一个缓存队列 下面是一个缓存队列的基本结构

image_1ckc8btt31eftpo66tvgja139e9.png-73kB

交换区 bindingKey跟queue构成channel

1.生产者
第一部分是生产者并,生产者不知道到底有什么队列可以存东西 只是一个单纯的搬运工 将打上了bindingKey的信息转发给第二部分exchange交换区

2.交换区
第二部分是交换区 交换区位于生产者和队列之间 所有的消息都是由交换区转发给队列的 转发给队列之间需要进行QueueBind队列绑定 类似下面

1
2
3
4
5
6
7
8
9
err = ch.QueueBind(
//用来接收信息的queue的名字
q.Name,
//bingdingKey是什么
b.String(),
//交换区是什么
EXCHANGE,
false,
nil)

简而言之 队列绑定就是当接收方声明一个队列作为接收队列的时候 用来说明 打上了什么样的bingdingKey的信息应该从交换区那边转发给给我这个队列里面 然后随后我自己会从这个队列里面拿出这些信息

3.队列
第三部分是队列 就是作为存储的空间 所有有待被接收方接收的信息都存储在这些队列里面 当一个信息被接收方接收成功之后 队列会删除这个信息以释放内存 如果接收方没有成功接收 那么这个信息会在队列里面重新排队 如果刚好有空闲的队列 那么这个信息会接着很快被处理 但是这里涉及到一个问题

队列是怎样知道接收方成功接收了信息呢?

详情如下

但是一旦发送方这边挂了 怎么办呢?

详情如下

公平分发 有时候有些种情况是当exchange类型是默认类型的话 需要分发信息12345 奇数的信息需要处理的时间更久一点 偶数处理时间比较短 那么还是轮流来分配对于第一个消费者来书是不公平的

如何实现公平分发

4.消费者
第四部分是消费者 消费者是可以多个的 消息队列的一个很大的好处是平行化工作 设想一下有一种情况是生产者不断生产工作 发送到队列里面 多个消费者从队列里面获取工作信息进行工作 这样的话可以大到平行化工作的效果 最大使用服务端这边的资源


exchange类型

exchange的集中类型

  • 默认类型 用一个””空字符串进行声明 在publish发送信息的时候需要指定一个队列的名字进行转发 如果有多个队列名字一样的接收方 那么将进行轮训转发 例如有12345需要转发 有两个队列名字一样的接收方 那么一个接受135 一个接受24
  • fanout 扇出类型 将信息发给所有可用的队列
  • direct类型 direct类型根据唯一的bindingKey将信息转发到进行了队列绑定的队列上 通俗地说就是打上了一个标签的信息会被转发到唯一的一个队列上
  • topic类型 topic类型是direct类型的进化版本 每个队列可以匹配一个或者多个标签

各种类型的交换区的使用方法有点啰嗦 这里就不讲了 可以去看官方文档 文档比较多例子 通俗易懂
rabbitMQ go文档

但是后面我会拿我这次在用户关系图谱这个项目上使用的rabbitMQ作为例子 我使用了direct类型的exchange

rabbitMQ在服务端上使用


队列怎么知道接收方成功接收

这里跟计算机网络里面的ACK是类似的 当接收方成功接收到了一个信息 会为这个信息给队列发送一个ACK信号说明我成功接收到了这个信息 在官方文档的hello world里面是设置了默认的自动发送ACK信号
微信图片_20180808161057.png-13kB

这个ACK机制其实是为了预防接收方挂了 因为一旦接收方不小心挂了 那么发送方这边就接收不到ACK信号 从而重新给那个信息排队

你也可以设置autoAck为false 然后自己在接收方那边使用

d.Ack(false)

来进行发送ACK信号


发送方不小心挂了怎么办

这就设置可靠性的设置了

首先需要设置队列是可靠的 durable可靠性这个选项是需要在服务端和客户端两边都需要设置的
微信图片_20180808161610.png-9.2kB

然后设置我们的信息是persistent持久的
微信图片_20180808161855.png-9.2kB

上面两个设置可以有效防止发送方这边挂了的情况 这样即使发送方不小心崩溃了 在重启之后也会重新发送还没发送成功的信息


公平分发

有时候有些种情况是当exchange类型是默认类型的话 需要分发信息12345 奇数的信息需要处理的时间更久一点 偶数处理时间比较短 那么还是轮流来分配对于第一个消费者来书是不公平的

1
2
3
4
5
6
err = ch.Qos(
1, // prefetch count
0, // prefetch size
false, // global
)
failOnError(err, "Failed to set QoS")

其实这样设置一下就可以了 这样表明队列是不会一次性地给一个消费者分发超过一个的消息 直到消费者处理完并且发送了前一个信息的ack信号之后再分发 或者将这个信息发送给下一个不是很忙的消费者


rabbitMQ在服务端上使用

项目部分代码地址
用户关系图谱路由server

其中rabbitMQ主要使在cache模块
进行rabbit的初始化
一定要注意如果你是在init函数中进行rabbit的初始化的话一定不要在init函数了里面使用defer进行关闭连接跟channel 因为这样的话后面要使用rabbit资源就会报错显示连接或者channel断开 rabbit资源的释放需要在其后关闭 最好需要关闭

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
func init() {
//加载rabbitMQ在配置文件中的配置
rabbitUser := beego.AppConfig.String("rabbitUser")
rabbitPsw := beego.AppConfig.String("rabbitPsw")
rabbitIp := beego.AppConfig.String("rabbitIp")
rabbitPort, _ := beego.AppConfig.Int("rabbitPort")
dbUrl := fmt.Sprintf("amqp://%s:%s@%s:%d/", rabbitUser, rabbitPsw, rabbitIp, rabbitPort)
var err error
//建立链接
if conn, err = amqp.Dial(dbUrl); err != nil {
panic(err)
}
//声明一个channel
if ch, err = conn.Channel(); err != nil {
panic(err)
}
//声明一个类型为direct的交换区
err = ch.ExchangeDeclare(
EXCHANGE,
"direct",
true,
false,
false,
false,
nil,
)
if err != nil {
panic(nil)
}
}

发布新消息
发布信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func PublishMsg(json []byte, bingdingKey BindingKey) error {
err := ch.Publish(
//指定我们要使用的direct类型的交换区
EXCHANGE,
//根据bingdingKey进行转发
bingdingKey.String(),
false,
false,
amqp.Publishing{
//发送的信息时候持久的 即使发送方突然挂了 重启之后还会继续发
DeliveryMode: amqp.Persistent,
//发送的类型是json对象
ContentType: "application/json",
Body: json,
})
return err
}

接受信息
接受信息
接收方因为用户关系图谱这个项目的逻辑比较复杂 所以写的说起来有点麻烦

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67


func GetMsg() {
//在这里释放rabbit的资源
defer conn.Close()
defer ch.Close()
forever := make(chan bool)
for _, bingdingKey := range AllBindingKeys {
go func(b BindingKey) {
fmt.Println("queue for", b)
//声明一个匿名队列
q, err := ch.QueueDeclare(
"",
true,
false,
true,
false,
nil,
)
if err != nil {
panic(err)
}
//进行队列绑定
err = ch.QueueBind(
q.Name,
b.String(),
EXCHANGE,
false,
nil)
if err != nil {
panic(err)
}
//声明一个消费者 从这个匿名队列里面读取信息
msgs, err := ch.Consume(
q.Name, // queue
"", // consumer
false, // auto ack
false, // exclusive
false, // no local
false, // no wait
nil, // args
)
if err != nil {
panic(err)
}

store := session.GetGraph()

switch b {
case CreateGroupShareLink:
for d := range msgs {
cgsl := models.CreateGroupShareLink{}
if err := json.Unmarshal(d.Body, &cgsl); err == nil {
fmt.Println("add CreateGroupShareLink", cgsl)
fmt.Println(cgsl.AddCreateGroupShareLinkToCayley(store))
} else {
panic(err)
}
//发送ack信号 代表已经接受成功
d.Ack(false)
}
//省略部分 太长省略
}
}(bingdingKey)
}
<- forever
}

释放rabbit资源

如果没有释放rabbit的资源 而且设置了channel和queue为可靠以及持久性的话 一般会导致一个bug

receiveed unexpected response

这是因为有可能在你挑食的时候突然关闭 而你却没有在代码里面主动close掉connection和channel 那么下一次运行的时候 接收方会重新发送一次一个信息 这导致发送方不知道这是什么ACK的信息 从而导致这个报错 一般来说

1
2
3
//在这里释放rabbit的资源
defer conn.Close()
defer ch.Close()

可以有效解决这个问题 但是还是会偶尔出现的 出现的话就重新开几次 两三次之后缓存被清理掉就没有这个报错了

mux学习

mux简介

mux implement了一个请求路由器和调度符合要求的输入请求到相应的视图函数中,mux是http request multiplexer 就是http多路复用器

mux的特点是

  • implement了http.Handler接口 因此跟http.ServeMux是兼容的
  • 请求可以基于URL path path前缀 协议 头部 还有请求参数 http方法 或者使用客制的匹配器
  • URL host, path 还有query values可以使用正则表达式来匹配变量
  • 注册URL可以被构造 或者”反向” 这样有助于维护队资源的引用
  • 可以使用子路由 嵌套路由只有当父路由被匹配之后才会被指向 有利于路由的分类 以及在视图函数的编写上遵循模块化编程的准则

install

go get -u github.com/gorilla/mux

hello world

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import (
"github.com/gorilla/mux"
"net/http"
"log"
)

func test(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("lalalala"))
w.WriteHeader(http.StatusOK)
}

func main() {
r := mux.NewRouter()
//r.Host("localhost")
r.HandleFunc("/test", test)
log.Fatal(http.ListenAndServe("localhost:8080", r))
}

基本使用

声明一个路由

r := mux.NewRouter()

注册视图函数

1
2
3
r.HandleFunc("/products/{key}", ProductHandler)
r.HandleFunc("/articles/{category}/", ArticlesCategoryHandler)
r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler)

注意到上面的URL匹配那里是可以使用正则表达式来进行匹配变量的

视图函数编写

1
2
3
4
5
func ArticlesCategoryHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "Category: %v\n", vars["category"])
}

视图函数或者说handler都是两个默认参数

  1. w http.ResponseWriter
  2. r *http.Request

要注意第二http.request请求的参数指针类型的 不要写错 其它跟Java的servlet基本相同 使用writer讲返回内容输出到http response中 但是要注意视图函数都是void类型 没有返回值


路由匹配

匹配域名

r.Host("www.example.com")

匹配一个动态的子域名 可以使用正则

r.Host("{subdomain:[a-z]+}.domain.com")

匹配URL前缀

r.PathPrefix("/products/")

匹配http方法

r.Methods("GET", "POST")

匹配协议

r.Schemes("https")

匹配头部

r.Headers("X-Requested-With", "XMLHttpRequest")

匹配查询参数

r.Queries("key", "value")

使用客制匹配器

1
2
3
r.MatcherFunc(func(r *http.Request, rm *RouteMatch) bool {
return r.ProtoMajor == 0
})

上面的各种匹配是可以一起声明的 向下面这样

1
2
3
4
r.HandleFunc("/products", ProductsHandler).
Host("www.example.com").
Methods("GET").
Schemes("http")


mux路由匹配冲突问题

如果一个请求跟两个路由都匹配 这就造成了冲突 这时候第一个会被成功匹配


子路由

子路由不仅仅很方便 而且有利于优化代码

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import (
"github.com/gorilla/mux"
"net/http"
"log"
)

func test(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("lalalala"))
w.WriteHeader(http.StatusOK)
}

func testSubRouter(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("this is subrouter page"))
vars := mux.Vars(r)
key := vars["key"]
w.Write([]byte(key))
w.WriteHeader(http.StatusOK)
}

func main() {
r := mux.NewRouter()
s := r.PathPrefix("/hello").Subrouter()
s.HandleFunc("/{key}", testSubRouter)
r.HandleFunc("/test", test)
log.Fatal(http.ListenAndServe("localhost:8080", r))
}


静态文件

静态文件在mux中一般使用static的url前缀来匹配 这样非常方便


未完待续!

参考链接

go web

前言

参考链接
这个教程是原生的库net/http来进行web服务端开发的 教程中使用了html/template来加载HTML模板 模板的语法也跟Python的jinja2的数据绑定很类似 但是我们清楚这种前后端不分离的方式其实不再合适未来的趋势


视图函数

go原生的net/http比较有意思 它的视图函数没有返回值也是可以的 它的http.HandleFunc这个装饰器可以默认返回空 不会报错 相反Python的一些框架就不行

一般视图函数格式 http.ResponseWriter用来写入返回给客户端的头部信息或者[]byte类型数据 *http.Request注意是一个指针类型 其实这里我有点不是很明白 怎么request是按照指针传递 response是按照值传递?

我自己觉得应该是在go语言中 response是从开发人员自己生命定义的handle视图函数开始就被一层一层的wrapper包装 所以response是不断被层层包裹被修改的 在每一层wrapper中response都不一样 所以按值传递就可以 但是requestsh1i是在各层wrapper或者redirect过程中都需要保持不变的 因为request是的参数被修改的话会影响http response这样没有太大意义 所以request最好使用指针传递 原来的request该怎样就怎样

stackoverflow的上关于这个问题的解释是

  • http.request是一个很大的结构 值拷贝的话花销非常大
  • http.request是含有一些状态state的 如果被拷贝的话具有一定的迷惑性
1
2
3
4
5
func handler(w http.ResponseWriter, r *http.Request)  {
p := r.URL.Path
fmt.Println("path", p)
w.Write([]byte("lalallallala"))
}

注册视图函数

1
http.HandleFunc("/", handler)

运行web后端app

1
http.ListenAndServe(":8080", nil)

上面是不会打印输出任何内容的 所以一般下面这样运行

1
log.Fatal(http.ListenAndServe(":8080", nil))

一般ip地址是可以被忽略的 默认是本机的IP地址

一般来说我们希望运行多个实例 然后在nginx上可以设置负载均衡 但是刚刚说上面那个监听运行语句是阻塞的 所以go语言这种天生支持多线程的的语言的优势就派上用场了

可以使用goroutine多开线程运行多个web app

1
2
3
4
5
for i := 0; i < 4; i++ {
go func() {
log.Fatal(http.ListenAndServe(":808" + string(i), nil))
}()
}

html模板加载和渲染就不记录了 就是使用原生的html/template这个库 语法非常类似Python的jinja2 感觉这部分在日益前后端分离的今天有点不合时宜了 所以只是看了看 没有多加记录

go多线程

注意

go是默认一个核只在一个时间点只可以运行一个线程 如果要实现并发 需要告诉go我们允许同时使用多个核 这样才可以实现真正意义上的并发


goroutine

go语言可以使用go开始执行一个新的线程 完果没有go的语句就跟正常一样 当所有的代码执行完毕而还有goroutine还没执行完 那么那些线程都会被终止

1
2
3
4
func main()  {
go fmt.Println("lalala")
fmt.Println("hahaha")
}

上面的情况是 其实只用了一个核 所有这不是并行 这是并发 那么输出结果是取决于那个goroutine结束最快 第二行输出结束最快(这里跟异步的感觉有点像) 那么输出是第二行 然后第二行运行结束之后 就终止了所有的goroutine 这时候第一行就来不及执行了 第一行等不到结束就被终止了

如果需要两行都输出 那么需要sleep一下 当第二行执行完的时候 这时候sleep会释放出CPU资源 这时候第一行可以使用CPU资源进行执行输出结果

1
2
3
4
5
func main()  {
go fmt.Println("lalala")
fmt.Println("hahaha")
time.Sleep(4)
}

输出
image_1chi83vnr1r2417ginpa12s9118c9.png-14kB

goroutine应用在func上

1
2
3
4
5
6
7
8
9
10
11
12
func publicMews(text string, delay time.Duration)  {
go func() {
time.Sleep(delay)
fmt.Println("breaking news", text)
}()
fmt.Println("after func")
time.Sleep(3)
}

func main() {
publicMews("world cup", 12)
}

结果跟上面类似


管道的基本使用

声明管道

声明双向的无缓存管道

test := make(chan int)

声明双向的有缓存管道

test2 := make(chan int, 10)

创建只写管道

test3 := make(chan <- int)

创建只读管道

test4 := make(<- chan int)

也可以声明通道是指针类型

test5 := make(<- chan *int)

使用管道在线程之间进行通信

当一个管道被声明之后 它是开着的 如果没有信息传进去 那么等待管道的线程一直等不到输出 会陷入死锁

只要另外一个线程的管道没有被正确关闭 那么主线程会一直等待 这样会陷入死锁

像下面这样

1
2
ch := make(chan string)
fmt.Println(<- ch)

输出显示死锁
image_1chiabekv14kl1lat12lm1nqkmlf1m.png-31.7kB

但是即使我们没有在管道中输入信息 但是及时关闭了管道 这样是可以避免出现死锁的出现的

像下面这样

1
2
3
ch := make(chan string)
close(ch)
fmt.Println(<- ch)

这样就没有造成死锁问题了

下面的例子 第一次发送之后我们关闭这个管道 那么第一次接受是可以接受到发送过来的内容的 然后因为我们关闭了这个管道 那么接下来的输出都是输出空字符串 同时因为已经正确关闭了管道 所以不会出现上述的死锁问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import "fmt"

func main() {
ch := make(chan string)
go func() {
ch <- "lalalalla"
close(ch)
}()
fmt.Println(<- ch)
fmt.Println(<- ch)
fmt.Println(<- ch)
a, b := <- ch
fmt.Println("a", a, "b", b)
}

输出
image_1chib44pt1faa1t1crb81v2a22j23.png-16.9kB

管道使用的时候一定要注意发送方和接受方一定要处于两个不同的线程 如果处于一个线程 会有一个莫名其妙的死锁报错

像下面这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import "fmt"

func sendMsgToCh(ch chan int) chan int {
go func() {
for i := 0; i < 4; i++ {
ch <- i
close(ch)
}
}()
return ch
}

func main() {
ch := make(chan int)
r := sendMsgToCh(ch)
for c := range r {
fmt.Println("receive value", c)

}
}

报错
image_1chk2d6gp1ffs16mg1o0hniitqe39.png-164.9kB


数据竞争

其实就是公有变量的互斥性访问问题 这个设置互斥锁问题

例如下面

1
2
3
4
5
6
7
8
9
10
11
func main()  {
ch := make(chan int)
n := 0
go func() {
n++
close(ch)
}()
n++
time.Sleep(3)
fmt.Println(n)
}

输出结果是 如果没有sleep三秒 那么输出一般都是1的 如果sleep之后输出是2 所以可见两个线程都是在做自增n的操作的

避免数据竞争的唯一方式是线程间同步访问所有的共享可变数据

在go语言中 这一般都是使用

  • 管道

有句话说的好

不要通过共享内存来通讯 而是通过通讯来共享内存(共享变量)

通过管道来实现两个线程同步修改公有变量

1
2
3
4
5
6
7
8
9
10
11
12
func main()  {
ch := make(chan int)
n := 0
go func() {
n++
ch <- n
close(ch)
}()
r := <- ch
r++
fmt.Println(r)
}


同步锁

向上面说的 出了使用管道来在进程间进行数据传输 还可以使用同步锁实现公有变量的互斥访问

下面是使用同步锁来让两个线程都自增一个公有变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package main

import (
"sync"
"fmt"
)

type AutomicInt struct {
mu sync.Mutex
val int
}

func (a *AutomicInt) add() {
a.mu.Lock()
a.val++
a.mu.Unlock()
}

func (a AutomicInt) get() int {
a.mu.Lock()
r := a.val
a.mu.Unlock()
return r
}

func main() {
mu := sync.Mutex{}
a := AutomicInt{mu, 0}
wait := make(chan struct{})
go func() {
a.add()
close(wait)
}()
a.add()
<- wait
r := a.get()
fmt.Println(r)
}

输出
image_1chk4qjrm2d31ljs4ve1nq56cf9.png-15.8kB


WaitGroup
相当于Python asyncio的eventloop 都是讲线程添加到一个队列进行阻塞运行 直至运行完毕 有几个函数要注意一下

声明一个waitgroup 并制定可以放1五个线程进去

wg := sync.WaitGroup{}
wg.Add(5)

上面这样声明之后 这个waitgroup的计数器就是5了 每当我们执行一个线程 需要在后面加上

wg.Done()

done函数会使得计数器-1 计数器减少到0之后就执行完毕

使用waitgroup进行多个线程互斥访问公有变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func race()  {
wg := sync.WaitGroup{}
wg.Add(5)
for i := 0; i < 5; i++ {
go func() {
fmt.Println("i", i)
wg.Done()
}()
}
wg.Wait()
}


func main() {
race()
}

输出是55555 这个结果很显然的了 就是for循环上的自增是先被执行的 后面的线程才被执行 那么所有线程输出的i都是5

可以在线程的匿名函数参数表chuan将公有变量作为参数穿进去 这样输出的结果才是对的 但是顺序肯定是乱的 因为线程的执行真的是顺序根本不确定的

如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func race()  {
wg := sync.WaitGroup{}
wg.Add(5)
for i := 0; i < 5; i++ {
go func(i int) {
fmt.Println("i", i)
wg.Done()
}(i)
}
wg.Wait()
}


func main() {
race()
}

输出结果
image_1chk5lond1u2k1vh68oj12glooq1m.png-15.8kB


select语句

golang的select跟select poll epoll相似 就是监听io操作 当思io操作发生时 触发相应的动作

注意select代码形式跟switch非常相似 但是select的case里操作语句只能是io操作

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"

func main() {
c1, c2 := make(chan int), make(chan int)
//var i1, i2 int
go func() {
c1 <- 12
close(c1)
}()
go func() {
c2 <- 23
close(c2)
}()
select {
case i1 := <- c1: fmt.Println("r 1", i1)
case i2 := <- c2: fmt.Println("r 2", i2)
}
}

上面的代码可以输出第一个 也可以额输出第二个 这样取决于哪个线程最先完成 select是一定要等到其中一个case有io操作 就是有数据传输过来的 再次之前一直阻塞

但是一直没有io语句执行成功 而且select含有default语句的时候 会执行default

select的作用感觉非常大 因为可以想象一个场景 一个服务器上有一个服务 这个服务有个处理入口 每个请求过来我们轮训select上每个io能否进行操作 如果有那么处理这个请求 如果没有那么去到default那里返回系统繁忙请稍后操作的提示 我们可以使用select轻易实现这个需求

go注意要点

数组的声明

当声明一个数组的时候 但是没有声明这个数组的大小 后续是可以自己通过append来往数组里加入新的元素的(其实那个就是切片slice)

1
2
3
4
5
6
7
func main()  {
a := []int{10, 100, 200}
a = append(a, 12)
for i := 0; i < 4; i++{
fmt.Println(a[i])
}
}

结构体

go的结构体跟c的结构体基本一样 但是比较有意思的是 go的结构体指针调用这个结构体的元素跟这个结构体调用元素都是一样的 都是以.一点

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
func main()  {
type test struct {
a int
b string
}
v := test{1, "123"}
fmt.Println(v.a)
fmt.Println(v.b)
v.a = 6
fmt.Println(v.a)
var structP *test = &v
fmt.Println(structP.a)
}


切片

go的切片跟Python的list切片一样 都是包含开始索引 但是不包含结束索引

例子

1
2
3
4
5
t := []int{1, 2, 3, 4, 5}
t = t[1: 3]
for i := 0; i < len(t); i++ {
fmt.Println(t[i])
}

len()函数但会切片的长度 cap()函数返回为切片分配的空间大小
copy()函数可以复制一个切片


range

之前一直被这个range弄得云里雾里 用法如下
for i, j := range sliceObject

i是下标 j是这个下标对应的元素


接口

go语言跟Java的接口不一样 go的接口定义方式就比价特别 是使用type interface两个关键字来声明的

接口一般都是搭配结构体来使用 一般都是一个结构体实现了这个接口的一些方法

例子
声明一个接口

1
2
3
type Phone interface {
call()
}

声明一个结构体

1
2
3
type iphone struct {
name string
}

这个结构体实现这个接口的一些函数(不用全部实现都可以 go的接口并不是implement整个接口 而是implement这个接口里的一些函数)

实现这个接口的一个函数

1
2
3
func (_iphone iphone) call() {
fmt.Println("this is iphone call")
}

调用

1
2
3
4
func main()  {
myIphone := iphone{}
myIphone.call()
}

接口中的方法理所当然是可以有返回类型的 上面那个例子是void类型的返回值 下面的例子是string类型返回值

1
2
3
4
5
6
7
8
9
10
11
type Phone interface {
receive() string
}

type iphone struct {
name string
}

func (_iphone iphone) receive() string {
return "this is some call from other phone in this phone " + _iphone.name
}

reflect反射

gp原因的反射是一种机制 可以在运行时更新变量和检查他们的值 调用他们的方法和他们支持的内在操作 而不需要在编译时就知道这些变量的具体类型


defer推迟

go这个defer关键字开始真的是看的我云里雾里 不知道用来干什么的 后来才知道这相当于一个延迟的”函数析构函数” 一般用来释放资源 关闭文件等等

而panic是在运行时捕捉到的类似数组访问月结 空指针引用等等问题 panic一般都会引起程序中断 是很严重的 所以一般都是使用error这个

panic引起程序中断之后 马上执行defer被延迟的函数来释放资源 以防因为程序中断而没有正确释放一些资源


json对象

讲go的结构体转为json对象的过程叫做编码 反过来叫解码

编码和解码分别使用下面两个函数

  • json.Marshal
  • json.Unmarshal

注意要点

  • 可以编码的结构体属性都是需要导出的 就是必须首字母是大写的
  • 编码之后的结果是[]uint8类型的 需要用fmt.Printf(“%s\n”, jData)这样的方法来进行打印 否则打印出来都是数字

struct tag

结构体的成员Tag可以是任意的字符串面值,但是通常是一系列用空格分隔的key:”value”键值对序列;因为值中含有双引号字符,因此成员Tag一般用原生字符串面值的形式书写。json开头键名对应的值用于控制encoding/json包的编码和解码的行为,并且encoding/…下面其它的包也遵循这个约定。

比如下面有个

Login bool `json:"login,omitempty"`

表示当结构体中的成员为空或者零值就不生成该结构体成员

果然 看下面的例子 Login这个属性 我给它的struct tag设置了omitempty 而且被编码的实例这个属性是false 编码为json之后就没有这个属性 但是比较有意思的是解码之后还是会有这个属性的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import (
"encoding/json"
"fmt"
)

type User struct {
Name string `json:"my_name`
Login bool `json:"login,omitempty"`
}

func main() {
u := User{"gzm", false}
jData, err := json.Marshal(u)
if err != nil {
fmt.Println("convert error")
}
fmt.Println(u)
fmt.Printf("%s\n", jData)
nu := User{}
err = json.Unmarshal(jData, &nu)
if err != nil {
fmt.Println("disonvert error")
}
fmt.Println(nu)
}

输出
image_1chqecnb4ur81770451lim1pvk16.png-6.7kB

有一个编码之后可读性很低的问题 因此可以用

json.MarshalIndent(u, "", "    ")

这个函数进行编码 第一个函数是结构体 第二个参数每一行输出的前缀 第三个参数是每一个级层的缩进

输出
image_1chqecvnngqeqmlvjnh7jut21j.png-1.9kB


byte和rune

其实byte和rune其实只是unint8和int32的别称

byte用来强调数据是raw data,而不是数字;而rune用来表示Unicode的code point。


方法

方法是面向对象编程中对对象或者结构体进行参数的一种方式 避免了直接对对象或者结构体的成员进行操作

go的结构体的方法声明和定义比较简单 只要在一个函数中有这个结构体作为参数传入 其后声明这个方法的名字和返回值类型 没有返回值类型则返回值类型为void

声明一个结构体

1
2
3
4
type MyUser struct {
Name string
Login bool
}

声明这个结构体的一个方法 名字为show 返回值类型为string

1
2
3
func (u MyUser) show() string {
return "name " + u.Name + " login " + strconv.FormatBool(u.Login)
}

调用这个方法

1
2
3
4
func main()  {
u := MyUser{"gzm", false}
fmt.Println(u.show())
}

输出
image_1chrrjj84tb4ebf9vu1cng1l4d9.png-26.9kB

要注意这个方法的参数列表的位置 上面那个u MyUser并不是这个参数列表 这个参数只是将结构体传进来 真正在参数列表在方法名字的后面
image_1chrro3fqfiac827p1v3a1ejgm.png-14.1kB

接收器 具有方法的结构体也叫作接收器

一般约定一个结构体或者类具有一个指针作为接收器的方法 但是go语言很人性化 传入结构体指针的方法 一般也可以直接使用这个结构体的变量去调用这个方法 编译器会隐式地转为指针类型


基本文件IO

一般文件的读写都是使用ioutil这个自带的库 写进去的内容和读出来的结果一般都是[]byte

讲byte数组写入文件

1
2
3
s := "lalla"
b := []byte(s)
err := ioutil.WriteFile("test.txt", b, 0600)

0600的意思是文件应该按照只有当前用户可以读写的权限进行创建这个文件

从文件中读内容为[]byte

1
2
3
4
5
rb, e := ioutil.ReadFile("test.txt")
if e != nil {
fmt.Println("read file error")
}
fmt.Println("file content", string(rb))


import _

之前这里一直想不明白为什么会有些import前面带有一个下划线的

import后面跟着一个_下划线意味着我们不需要import这个包的其他内容 我们只需要它执行这个包里面的init函数 所以这个下划线只是起到一个初始化的作用


go执行方式

image_1chuhkjdu1q171r73oem1jov1mla9.png-93.6kB

  1. 深度优先执行import
  2. 执行const
  3. 执行var
  4. 自行init函数

有点出乎意料 init函数竟然是在最后执行的

mongodb入门

看什么文档都是从基础入门开始的 不用太浮躁

mongodb跟sql概念上的差别
image_1ci8lckm31isl15ai1j9tjdo1rc69.png-25.4kB
mongodb是不支持表相连的


mongodb模式

mongodb使用动态模式 开发人员可以不定义表结构 有什么列 什么类型就可以创建一个表 只要我们往表中添加了新的列或者删除了已有的列 那么行的结构就会被修改 表中的行并不需要具有相同的成员或者说列


基础命令

清屏命令

ctrl + l

查询所有数据库

show dbs

切换到具体一个db

use cayley

展示所有集合

show tables

或者

show collections

是等价的

查询一个集合中所有单位document

db.myCollection.find()

dgraph图数据库

schema

dgraph的节点分为两类

  1. node节点
  2. value字面量

例如:

  • a b是两个人
  • a friend b
  • a age 12

那么a b是两个节点 12是字面量 那么friend跟age都是谓语 friend指向b b是一个node age指向12 12是一个字面量 那么谓语所处的边为什么有时候指向一个node 有时候指向一个字面量呢 这就是由模式所定义的 或者说模式就是边的定义或者说模式就是谓语的定义

1.添加模式

当我们项添加数据到已有的模式中 我们可以直接添加 但是我们需要添加新数据到还没有存在模式中的时候 可以有两种方式

  • 直接添加数据 让Dgraph自己找出其中的模式
  • 先添加一个新的模式 然后添加数据

在模式中我们需要定义索引 因为dgraph的函数跟filter过滤器都是只可以应用于添加了索引的谓语 不在模式中添加索引的话 那么各种查询进行不了

image_1ci6k5u3ir3810kr17jd1641jud40.png-28.8kB

2.查询模式

1
2
3
4
schema(pred: [name, age, friend, owns_pet]) {
type
index
}

输出 很显然下面的输出都是按照谓语一个一个输出这个谓语指向的那个的类型(是node还是字面量) 以及是否具有索引 如果为false就不输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
{
"data": {
"schema": [
{
"predicate": "age",
"type": "int",
"index": true
},
{
"predicate": "friend",
"type": "uid"
},
{
"predicate": "name",
"type": "string",
"index": true
},
{
"predicate": "owns_pet",
"type": "uid"
}
]
},
"extensions": {
"server_latency": {
"encoding_ns": 1000000
},
"txn": {
"start_ts": 20,
"lin_read": {}
}
}
}


dgraph支持的数据类型

  • int 有符号64位整数
  • float 双精度浮点数
  • string 字符串
  • bool 布尔值
  • id id是以字符串的形式进行存储的
  • dateTime 时间
  • geo 几何体

编码

dgraph是使用utf-8进行编码的 一些谓语也是使用字符串来存储的 可以在加上语言标签来定义是什么语言

节点加上语言标签进行声明

"Amit"@en

Amit这个名字是英文

多个语言匹配

@lang1:...:langN

上面这个语法的特点

  • 最多只有一个结果返回
  • 如果有结果出现在这个语言列表中 那么最左边那部分的匹配结果都会被返回
  • 如果没有结果匹配就没有返回 但是如果最后时以.为结尾的话例外 .以为着什么语言都行 匹配上了就可以返回

使用语言pipei匹配来查询一个人的朋友他的朋友名字各种语言都有

1
2
3
4
5
6
7
8
9
10
{
language_support(func: allofterms(name@hi, "अमित")) {
name@bn:hi:en
age
friend {
name@ko:ru:.
age
}
}
}

输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
{
"data": {
"language_support": [
{
"name@bn:hi:en": "অমিত",
"age": 35,
"friend": [
{
"name@ko:ru:.": "Michael",
"age": 39
},
{
"name@ko:ru:.": "상현",
"age": 24
},
{
"name@ko:ru:.": "Артём",
"age": 35
}
]
}
]
},
"extensions": {
"server_latency": {
"encoding_ns": 1000000
},
"txn": {
"start_ts": 58,
"lin_read": {
"ids": {
"1": 6
}
}
}
}
}


查询格式

其实上面的查询语句一直都是有规矩可循的 但是一直没有注意到

之前我们说到dgraph有两种节点

  • node节点
  • value字面量

常见的两种形式
image_1ci68dk4oo2r1gn4c3qpga18d152.png-10.3kB

谓语不属于节点

在查询上

  • 字面量的查询只需要把字面量的名称写入即可
  • node节点的查询需要把指向它的谓语和它的名称一起写入

如下 查询Michael的字面量: name age (这些都是他指向的字面量) 以及他的朋友(他的朋友就是他指向的节点)

image_1ci6539gh1jp8kvq1aj9j5610hl9.png-29.5kB

所以我们要进一步查询Michael的朋友的宠物 只要在上面的查询语句稍加修改即可
微信图片_20180712103548.png-20.7kB


过滤器

常见过滤器

1.节点上的过滤器

allOfTerms(node_name, "term1 ... termN")

节点的名字含有给出的所有单词

anyOfTerms(node_name, "term1 ... termN")

节点的名字函数给出的单词列表中的任意一个单词

2.边上的过滤器
边上的过滤器可以应用到int float string date类型的边上

  • eq(edge_name, value) 等于
  • ge(edge_name, value) 大于等于
  • le(edge_name, value) 小于等于
  • gt(edge_name, value) 大于
  • lt(edge_name, value) 小于等于

image_1ci698cfo30h1pi9cot8uh15ut5f.png-25.5kB

过滤器上可以使用逻辑AND OR NOT将过滤条件联合起来

1
2
3
4
5
6
7
8
9
10
{
michael_friends_and(func: allofterms(name, "Michael")) {
name
age
friend @filter(ge(age, 27) AND le(age, 48)) {
name@.
age
}
}
}

排序

可以使用下面两个关键字进行排序

  • orderasc 从小到大排序
  • orderdesc 从大到小排序

注意 排序只是在返回的json对象上有效果 在UI的可视化上是没效果的

image_1ci69hu24163uvv11q8f1a0eih15s.png-22.3kB


分页

  • first: N 只返回前N个结果
  • offset: N 跳过前N个结果
  • after: uid 返回在这个uid后面的结果

image_1ci6a1bp71l331f50jcsd7d189f79.png-20kB


count

dgraph的count用起来挺方便
image_1ci6a8vhm1gcv1bf0sfikeh1csk7m.png-13.8kB


根节点

因为有时候dgraph的图非常大 那么从所有节点开始搜索是很低效的 那么根节点的概念就出现了 从根节点开始搜索

在根节点处 我们是使用func:和一个函数来找到一个初始化的节点集合

但是root节点的那个函数func是不可以接受and or not那些逻辑操作的 所以如果需要在根节点处加上这些逻辑操作需要在后面自己加一个filter过滤器

image_1ci6e87bj1dr3lmjcotgm7aci83.png-18.9kB


根据谓语(边)查询

image_1ci6hoimm59ufol1nmu125u1snj8g.png-17.2kB


alias别名

别名可以用于设置返回的json对象中key的名字 改成自己想要设置成的别名
image_1ci6hs52rbcl10hj1s3kbd95m58t.png-20kB


级联cascade指令

级联的概念开始真的是有点难以理解 举个例子就容易多了

image_1ci6iv71ofb6jfdmlp1rasfjt9.png-15.1kB
上面的查询是查询Michael年纪大于等于70的朋友 Michael有年纪大于等于70的朋友就直接返回在friend的列表里 如果没有那么friend一项就直接为空

但是加上@cascade之后效果就不一样了
image_1ci6j4lgv16r38pugr4ll91isl1m.png-51.3kB

上面加上了@cascade之后 如果Michael如果有年纪大于鞥与70的朋友那么Michael跟她的朋友一起在返回的结果里面 但是如果没有 那么连Michael也不返回

因为级联就相当于强行的逻辑与如果后面的节点不满足要求 那么前面已经满足匹配的节点也不会被返回


normal指令

@normalize指令

  • 只返回带有别名的节点
  • 把结果扁平化 去除嵌套

image_1ci6jhmbl1ptpu9k130n1ot71q5p33.png-52.2kB


数据突变

数据突变就是修改存储在Dgraph中的图结构
image_1ci6knlvgq8l1tnb1fif1qiddev5d.png-81.9kB


External Identifiers外部标识符

Dgraph是不支持为node设置外部标记符的 所以如果开发人员希望为节点添加一个外部标记符 那么需要自己添加一条边作为外部标记符


语言支持

这个语言标记上面已经减了很多次了 其中中文的标志使用以下来标志

_:myID <an_edge> "某物"@zh-Hans .

反向边

边是有向的一个查询是不可以反向遍历的 有两种方式可以双向进行查询

  • 添加反向边到模式中 添加所有的反向边数据
  • 在模式中使用@reverse关键字告诉Dgraph告诉Dgraph总是存储反向边
boss_of: uid @reverse .

整合已有数据

当我们第一批次向数据库中导入了一系列的人物关系数据 第二批次的时候导入了一系列的公司数据 那么问题在于我们怎么把这些人物和公司关联起来呢?

答案肯定是添加新的模式 但是添加新的模式要注意 因为我们肯定不能在新的模式中使用下面

_:sarah <works_for> _:company1 .

因为这样的话会为z这个sarah新建一个uid 而不是我们之前的那个sarah 这是一个新的sarah 正确的做法应该是

<uid-for-sarah> <works_for> <uid-for-company1> .

找到sarah和company的uid根据uid定义这些已存在节点之间的新模式

例子

1
2
3
4
5
{
set {
<0x2> <boss_of> <0xd> .
}
}


删除数据

删除数据有三种

方式

  • /“value” . 根据宾语的node的uid或者宾语中的字面量的值删除一个三元组
  • * . 根据给定的一条边删除所有的三元组
  • . 根据给定的一个node 删除所有的三元组

image_1ci6mejsl1l8t33ophi167a1j585q.png-12.4kB


谓语查询

可以根据一个node节点查询这个节点所有的谓语
使用

_predicate_

image_1ci6migc0iel3o5v010j114hv67.png-52.3kB


拓展谓语

expand(_all_)

上面是查询所有的谓语 拓展谓语是根据给定的谓语进行深入查询 而不是单纯在返回结果中列出他们

image_1ci6mt3qg17abbh479c17cp1n1i74.png-40.4kB


多个查询块

没什么区别 就是多几个查询根节点而已
image_1ci6p35ni1idvl5j13ja12q7qdg7h.png-26.7kB


查询变量

结果可以被存在变量里面 可以在查询的任意一个位置被使用