前言

之前的文章中提到mgo的使用方式是基于session对象的,关于mgo与session,参考旧文mgo的session与连接池。本文将会探讨我们在生产中是具体如何使用这个库的。不意在确定一种万用的“正确”方法,而是记录一下方便日后批评与自我批评(XD)。

当然,严格来说,这个问题跟golang语言的使用没啥关系;但我就是觉得之前的使用方法很枯燥,好气哦!

stateless的包方法使用

顾名思义,就是将mongo的各种数据库操作写成一个个方法调用,比如要根据id查找一条文档,那么就有如下的exported package function

package mongo

func FindById(id) (DocStruct, err){
	session := GETSESSION()
	defer session.Close()
	Doc := DocStruct{}
	err := session.DB.C.Find({},&Doc)
	return Doc, err
}

使用的时候就是在业务代码中,调用一下这个方法。


func () {
	doc ,err:= mongo.FindById(id)

}

这种操作,我自己称之为无状态(stateless)的数据库操作,因为这种操作只是一个动作,他也不会涉及到管理任何的连接或者数据状态。当然这种写法是没什么问题,在操作简单、固定的情况下;吹毛求疵的话,就是session的行为不太好控制。

但是当数据库操作变得复杂,一个结构的行为多样化的时候,就会产生很多这样的方法。比如需要根据id,对某个字段CRUD,那就要产生四个方法,在业务代码中进行组合。还有一些更复杂的针对mongo 嵌套文档的CURD就更是如此了。这样的问题第一是,要产生多个session对象,其实完全没必要,(session获得连接的效率问题在旧文中也提到过);另一个是操作很多,操作过于松散,总感觉不好维护。有时候甚至会忘记某个操作之前实现过,而写了个差不多得方法满足新的需要。

新项目中的尝试

首先上帝说要有对象

在目睹了一个包中,针对无数个文档有无数野生方法的惨状以后,我决定放弃这种exported package function的类C用法。我觉得使用struct抽象出对象模型,每个对象的声明周期内,都会持有一个session,在对象结束时使用session。这样的话,一个文档生命周期内所有的操作,就并不必产生多余的session对象,对mgo的并发问题也更加友好。struct拥有自己的CURD操作,感觉也更规整一些。

具体我是这么做的。

有一个以文档为单位的interface{}


type IDoc interface {
	DB () string
	Col() string
	Close()
}

假设有一个mongo collection存储着数据模型为Person的数据


type Person struct{
	Id bson.ObjectId `bson:"_id"`
	Name string `bson:"name"`
	Age int `bson:"age"`
}

首先规定所有的文档结构定义都要实现IDoc的接口,当然其实这个卵用不大,只是希望规范一下模型;具体用处下文细讲。

其实不实现也没问题,IDoc不是作为一个操作抽象存在的;但我还是会在全局定义的地方写

var _ IDoc = &Person{}

保证实现了方法。

然后上帝说要善待session

定义了一个包装mgo.Session的结构体。(我这次不希望mgo的import在我的项目里到处跑)

type SessionHelper struct {
	*mgo.Session
}

func (s *SessionHelper)CloseHelper() {
	s.Close()
}

重新定一下Person,让他持有一个SessionHelper


type Person struct{
	SessionHelper `bson:"-"`
	Id bson.ObjectId `bson:"_id"`
	Name string `bson:"name"`
	Age int `bson:"age"`
}

之后就可以基于这样的结构,实现任意的CRUD方法,比如:

func (p *Persion) GetName() ()string,error){
	p.DB(p.DB()).Col(p.Col()).Find(bson.M{...}).Select(bson.M{...})
	...

对于业务代码来说,复杂的sql在method receiver上去现实,外部不关心的操作可以匿名;对session非常友好。

当然,doc还需要New方法

func NewPerson() *Person{
	return &Person{	
		SessionHelper:GETSESSION(),
	}
}

使用时候的画风就是

func foo() {
	p := NewPerson()
	defer p.Close()
	p.LoadById(id)
	p.GetName()
	...
	p.DoXXX()

}

反正我觉得比如下这种nice很多。包方法毕竟是共享名字的,有时候不看代码也搞不太清一个方法到底是要操作哪个数据结构。


func foo() {
     p := &Person{}
     p.Name = mongo.FindName(id)
     p.xxx = mongo.Findxxx()
    ...
}

一个缺点

大概一个缺点就是,因为集成了session,每个对象在生成以后必须手动Close,否则会造成mgo的连接泄露。其实这跟每次生成session的方法是一样的,也是要成对生成释放的,也没差。

爬坑时间

golang的作用域

事情大概是这样的,有一个大文档,里面引用了某些小文档的id,(我采用的方式是手动引用,mongo其实提供了一种reference方法)我觉得没差就没用。

引用小对象的话,CURD之前先要确定这些小对象存不存在,那就有了如下的过程

func (d *Doc) Foo() {
	obj1 := NewObj1()
	defer obj1.Close()
	obj1.Load(id)
	obj1.Exists()
	
	obj2 := NewObj1()
	defer obj2.Close()
	obj2.Load(id)
	obj2.Exists()
	
}

这当然也没什么问题,但obj1,obj2的声明周期过于长了,在这个过程中,会有三条mongo的连接被使用到,其中两个释放要等到整个函数结束。显然太浪费了。

于是我一抠脚,写出了如下的代码:


func (d *Doc) Foo() {
	{
	obj1 := NewObj1()
	defer obj1.Close()
	obj1.Load(id)
	obj1.Exists()
	}
	
	{
	obj2 := NewObj1()
	defer obj2.Close()
	obj2.Load(id)
	obj2.Exists()
	}
	
}

十秒钟以后,我意识到这是没有任何意义的;因为goalng的defer是注册在函数栈上的,跟作用域没有半毛钱关系,这两个defer照样是在函数返回的时候执行的。

其实到这里,封装成两个函数,缩小一下函数栈就可以达到控制生命周期释放连接的目的;但是老师,我不服,我有看上去更酷炫的写法。

十秒钟以后,这个函数变成了这样


func (d *Doc) Foo() {
	{
	obj1 := NewObj1()
	runtime.SetFinalizer(obj1,func(obj interface{}){
		obj1.Close()
	})
	obj1.Load(id)
	obj1.Exists()
	}
	
	{
	obj2 := NewObj1()
	runtime.SetFinalizer(obj1,func(obj interface{}){
		obj2.Close()
	}
	obj2.Load(id)
	obj2.Exists()
	}
	
}

这样就看上去给力多了是不是!runtime.SetFinalizer会在运行时发现注册对象失去引用的时候调用回调方法。

但是!现实是很残酷的,这段代码其实完全不给力,因为运行时发现对象失去引用是需要一段时间的,这段时间甚至比defer版本更久~

那老师!就没有更给力的了吗。当然有!

func (d *Doc) Foo() {
	{
	obj1 := NewObj1()
	runtime.SetFinalizer(obj1,func(obj interface{}){
		obj1.Close()
	}) 
	obj1.Load(id)
	obj1.Exists()
	}
	runtime.GC()
	
	{
	obj2 := NewObj1()
	runtime.SetFinalizer(obj1,func(obj interface{}){
		obj2.Close()
	}
	obj2.Load(id)
	obj2.Exists()
	}
	
	runtime.GC()
	
}

每当我想要弄死的对象出了作用域,就调用一次强制GC,连接就会毫不犹豫立马释放。

不过最后,我还是为这个Doc实现了小函数来判断引用是否存在;因为这个世界上存在一种行为叫code review,写这种代码会被搞事情的!(逃)

bson.Unmarshal的置空行为

使用这种对象模型,在查询上面很容易地想到就是用自己的引用来反序列化

比如

func (p *Persion)Load(id) {
	p.SessionHelper.Find(...).One(p)
	 
}

这个函数的预期就是把所有字段查询出来,再从bson格式反序列化到p的各个字段。但是,这个mgo下的bson实现有一个我现在还不太理解的动作,会将传入反序列化的实例置为空,而且是无视tag的粗暴的置为空。

也就是说,假设我的结构里面有一些跟数据库字段无关的bson:"-"的辅助字段,用于保存一些计算的结果之类的。

例如:


type Person struct {
	...
	Field string `bson:"-"`
}

mgo这个bson老哥会把我的Field字段置为空。那好气哦。 内部调用的方法就是bson.Unmarshal,具体里面的实现这里就不多说了,代码很好找;这么做原因不懂,提了issue也没人理(好气QAQ)。 没人理的issue

结语

希望mongo官方早日能出自己的驱动!