配色: 字号:
如何设计一个简单的C++ ORM
2016-12-01 | 阅:  转:  |  分享 
  
如何设计一个简单的C++ORM

2016-12-0114:28byBOT-Man,184阅读,4评论,收藏,编辑

2016/11/15



“没有好的接口,用C++读写数据库和写图形界面一样痛苦”



阅读这篇文章前,你最好知道什么是

ObjectRelationMapping(ORM)



阅读这篇文章后,欢迎阅读下一篇如何设计一个更好的C++ORM

??



为什么C++要ORM

Asgoodobject-orienteddevelopersgottiredofthisrepetitivework,

theirtypicaltendencytowardsenlightenedlazinessstarted

tomanifestitselfinthecreationoftoolstohelpautomate

theprocess.



Whenworkingwithrelationaldatabases,

theculminationofsucheffortswereobject/relationalmappingtools.



一般的C++数据库接口,都需要手动生成SQL语句;

手动生成的查询字符串,常常会因为模型改动而失效;

查询语句/结果和C++原生数据之间的转换,每次都要手动解析;

我为什么要写ORM



C++大作业需要实现一个在线的对战游戏,其中的游戏信息需要保存到数据库里;



而我最初始的里没有使用ORM导致生成SQL语句的代码占了好大一个部分;

并且这一大堆代码里的小错误往往很难被发现;



每次修改游戏里怪物的模型都需要同步修改这些代码;

然而在修改的过程中经常因为疏漏而出现小错误;



所以,我打算让代码生成这段代码;??



市场上的C++ORM

大致可以分成这几类:



使用预编译器生成模型和操作:

ODB

LiteSQL

使用宏生成模型和操作:

sqlpp11

需要在定义模型时,通过手动插入代码进行注入:

HiberliteORM

OpenObjectStore

Wt::Dbo

QxORM(Qt风格的庞大。。)

以上的方案都使用了

ProxyPattern



AdapterPattern

实现ORM功能,并提供一个用于和数据库交换数据的容器;



所以我打算封装一个直接操作原始模型的ORM;??



一个简单的设计——ORMLite

关于这个设计的代码和样例??:

https://github.com/BOT-Man-JL/ORM-Lite/tree/v1.0



0.这个ORM要做什么



将对C++对象操作转化成SQL查询语句

(LINQtoSQL);

提供C++Style接口,更方便的使用;

我的设计上大致分为6个方面:



封装SQL链接器

遍历对象内需要持久化的成员

序列化和反序列化

获取类名和各个字段的字符串

获取字段类型

将对C++对象的操作转化为SQL语句

1.封装SQL链接器



为了让ORM支持各种数据库,

我们应该把对数据库的操作抽象为一个统一的Execute:



classSQLConnector

{

public:

SQLConnector(conststd::string&connectionString);

voidExecute(...);

};

一个有点意思的接口设计——

std::db

因为SQLite比较简单,目前只实现了SQLite的版本;

MySql版本应该会在

这里维护。。。??

2.遍历对象内需要持久化的成员



2.1使用VisitorPattern+VariadicTemplate遍历



一开始,我想到的是使用

VisitorPattern

组合

VariadicTemplate

进行成员的遍历;



首先,在模型处加入__Accept操作;

通过VISITOR接受不同的Visitor来实现特定功能;

并用__VA_ARGS__传入需要持久化的成员列表:



#defineORMAP(_MY_CLASS_,...)\

template\

void__Accept(VISITOR&visitor)\

{\

visitor.Visit(__VA_ARGS__);\

}\

template\

void__Accept(VISITOR&visitor)const\

{\

visitor.Visit(__VA_ARGS__);\

}\

然后,针对不同功能,实现不同的Visitor;

再通过统一的Visit接口,接受模型的变长数据成员参数;

例如ReaderVisitor:



classReaderVisitor

{

public:

//DatatoExchange

std::vectorserializedValues;



template

inlinevoidVisit(Args&...args)

{

_Visit(args...);

}



protected:

template

inlinevoid_Visit(T&property,Args&...args)

{

_Visit(property);

_Visit(args...);

}



template

inlinevoid_Visit(T&property)override

{

serializedValues.emplace_back(std::to_string(property));

}



template<>

inlinevoid_Visit(std::string&property)override

{

serializedValues.emplace_back("''"+property+"''");

}

};

Visit将操作转发给带有变长模板的_Visit;

有变长模板的_Visit将各个操作转发给处理单个数据的_Visit;

处理单个数据的_Visit将模型的数据

和Visitor一个public数据成员(serializedValues)交换;

不过,这么设计有一定的缺点:



我们需要预先定义所有的Visitor,灵活性不够强;

我们需要把和需要持久化的成员交换的数据保存到Visitor内部,

增大了代码的耦合;

2.2带有泛型函数参数的Visitor



(使用了C++14的特性)



所以,我们可以让Visit接受一个泛型函数参数,用这个函数进行实际的操作;



在模型处加入的__Accept操作改为:



template\

void__Accept(constVISITOR&visitor,FNfn)\

{\

visitor.Visit(fn,__VA_ARGS__);\

}\

template\

void__Accept(constVISITOR&visitor,FNfn)const\

{\

visitor.Visit(fn,__VA_ARGS__);\

}\

fn为泛型函数参数;

每次调用__Accept的时候,把fn传给visitor的Visit函数;

然后,我们可以定义一个统一的Visitor,遍历传入的参数,并调用fn——

相当于将Visitor抽象为一个foreach操作:



classFnVisitor

{

public:

template

inlinevoidVisit(Fnfn,Args&...args)const

{

_Visit(fn,args...);

}



protected:

template

inlinevoid_Visit(Fnfn,T&property,Args&...args)const

{

_Visit(fn,property);

_Visit(fn,args...);

}



template

inlinevoid_Visit(Fnfn,T&property)const

{

fn(property);

}

};

最后,实际的数据交换操作通过传入特定的fn实现:



queryHelper.__Accept(FnVisitor(),

[&argv,&index](auto&val)

{

DeserializeValue(val,argv[index++]);

});

对比上边,这个方法实际上是在处理单个数据的_Visit将模型的数据

传给回调函数fn;

fn使用

GenericLambda

接受不同类型的数据成员,然后再转发给其他函数(DeserializeValue);

通过capture和需要持久化的成员交换的数据;

2.3另一种设计——用tuple+Refrence遍历



(使用了C++14的特性)



虽然最后版本没有使用这个设计,不过作为一个不错的思路,我还是记下来了;??



首先,在模型处通过加入生成tuple的函数:



#defineORMAP(_MY_CLASS_,...)\

inlinedecltype(auto)__ValTuple()\

{\

returnstd::forward_as_tuple(__VA_ARGS__);\

}\

inlinedecltype(auto)__ValTuple()const\

{\

returnstd::forward_as_tuple(__VA_ARGS__);\

}\

forward_as_tuple将__VA_ARGS__传入的参数转化为引用的tuple;

decltype(auto)自动推导返回值类型;

然后,定义一个TupleVisitor:



//Usinga_SizeTtospecifytheIndex:-),Cool

templatestruct_SizeT{};



template

inlinevoidTupleVisitor(TupleType&tuple,ActionTypeaction)

{

TupleVisitor_Impl(tuple,action,

_SizeT::value>());

}



template

inlinevoidTupleVisitor_Impl(TupleType&tuple,ActionTypeaction,

_SizeT<0>)

{}



template

inlinevoidTupleVisitor_Impl(TupleType&tuple,ActionTypeaction,

_SizeT)

{

TupleVisitor_Impl(tuple,action,_SizeT());

action(std::get(tuple));

}

其中使用了_SizeT巧妙的进行tuple下标的判断;

具体参考

http://stackoverflow.com/questions/18155533/how-to-iterate-through-stdtuple

最后,类似上边,实际的数据交换操作通过TupleVisitor完成:



autotuple=queryHelper.__ValTuple();

TupleVisitor(tuple,[&argv,&index](auto&val)

{

DeserializeValue(val,argv[index++]);

});

2.4问题



使用VariadicTemplate和tuple遍历数据,

其函数调用的确定,都是编译时就生成的,这会带来一定的代码空间开销;

后两个方法可能在实例化GenericLambda的时候,

针对不同类型的模型的不同数据成员类型实例化出不同的副本,

代码大小更大;??

3.序列化和反序列化



通过序列化,

将C++数据类型转化为字符串,用于查询;

通过反序列化,

将查询得到的字符串,转回C++的数据类型;



3.1重载函数_Visit



针对每种支持的数据类型重载一个_Visit函数,

然后对其进行相应的序列化和反序列化;



以序列化为例:



void_Visit(long&property)override

{

serializedValues.emplace_back(std::to_string(property));

}



void_Visit(double&property)override

{

serializedValues.emplace_back(std::to_string(property));

}



void_Visit(std::string&property)override

{

serializedValues.emplace_back("''"+property+"''");

}

3.2使用std::iostream



然而,针对每种支持的数据类型重载,这种事情在标准库里已经有人做好了;

所以,我们可以改用了std::iostream进行序列化和反序列化;



以反序列化为例:



template

inlinestd::ostream&SerializeValue(std::ostream&os,

constT&value)

{

returnos<
}



template<>

inlinestd::ostream&SerializeValue(

std::ostream&os,conststd::string&value)

{

returnos<<"''"<
}

4.获取类名和各个字段的字符串



我们可以使用宏中的#获取传入参数的文字量;

然后将这个字符串作为private成员存入这个类中:



#defineORMAP(_MY_CLASS_,...)\

constexprstaticconstchar__ClassName=#_MY_CLASS_;\

constexprstaticconstchar__FieldNames=#__VA_ARGS__;\

其中



#_MY_CLASS_为类名;

#__VA_ARGS__为传入可变参数的字符串;

__FieldNames可以通过简单的字符串处理获得各个字段的字符串

5.获取字段类型



在新建数据库的Table的时候,

我们不仅需要类名和各个字段的字符串,

还需要获得各个字段的数据类型;



5.1使用typeid运行时判断



在Visitor遍历成员时,将每个成员的typeid保存起来:



template

void_Visit(T&property)override

{

typeIds.emplace_back(typeid(T));

}

运行时根据typeid判断类型并匹配字符串:



if(typeId==typeid(int))

typeFmt="int";

elseif(typeId==typeid(double))

typeFmt="decimal";

elseif(typeId==typeid(std::string))

typeFmt="varchar";

else

returnfalse;

5.2使用编译时判断



由于对象的类型在编译时已经可以确定,

所以我们可以直接使用进行编译时判断:



constexprconstchartypeStr=

(std::is_integral::value&&

!std::is_same,char>::value&&

!std::is_same,wchar_t>::value&&

!std::is_same,char16_t>::value&&

!std::is_same,char32_t>::value&&

!std::is_same,unsignedchar>::value)

?"integer"

:(std::is_floating_point::value)

?"real"

:(std::is_same,std::string>::value)

?"text"

:nullptr;



static_assert(

typeStr!=nullptr,

"OnlySupportIntegral,FloatingPointandstd::string:-)");

constexpr编译时完成推导,减少运行时开销;

static_assert编译时类型检查;

6.将对C++对象的操作转化为SQL语句



这里,我们应该提供

FluentInterface:



autoquery=mapper.Query(helper)

.Where(

Field(helper.name)=="July"&&

(Field(helper.id)<=90&&Field(helper.id)>=60)

)

.OrderByDescending(helper.id)

.Take(3)

.Skip(10)

.ToVector();

6.1映射到对应的表中



对于一般的操作,通过模板类型参数,获取__ClassName:



template

voidInsert(constC&entity)

{

autotableName=C::__ClassName;

//...

}

带有条件的查询通过Query,将自动映射到MyClass表中;

并返回自己的引用,实现FluentInterface:



template

ORQueryQuery(constC&queryHelper)

{

returnORQuery(queryHelper,this);

}



ORQuery&Where(constExpr&expr)

{

//Parseexpr

returnthis;

}

6.2自动将C++表达式转为SQL表达式



首先,引入一个Expr类,用于保存条件表达式;

将Expr对象传入ORQuery.Where,以实现条件查询:



structExpr

{

std::vector>expr;



template

Expr(constT&property,

conststd::string&relOp,

constwww.baiyuewang.netT&value)

:expr{std::make_pair(&property,relOp)}

{

//Serializevalueintoexpr.front().second

}



inlineExproperator&&(constExpr&right)

{

//ReturnCompositeExpression

}



//...

}

expr保存了表达式序列,包括该成员的指针和关系运算符值的字符串;

重载&&||实现表达式的复合条件;

不过这样的接口还不够友好;

因为如果我们要生成一个Expr则需要手动传入conststd::string&relOp:



mapper.Query(helper)

.Where(

Expr(helper.name,"=",std::string("July"))&&

(Expr(helper.id,"<=",90)&&Expr(helper.id,">=",60))

)

//...

所以,我们在这里引入一个Expr_Field实现自动构造表达式:



template

structExpr_Field

{

constT&_property;



Expr_Field(constT&property)

:_property(property)

{}



inlineExproperator==(Tvalue)

{returnExpr{_property,"=",std::move(value)};}



//!=><>=<=

}



template

Expr_FieldField(T&property)

{

returnExpr_Field{property};

}

Field函数用更短的语句,返回一个Expr_Field对象;

重载==!=><>=<=生成带有对应关系的Expr对象;

6.3自动判断C++对象的成员字段名



由于没有想到很好的办法,所以目前使用了指针进行运行时判断:



queryHelper.__Accept(FnVisitor(),

[&property,&isFound,&index](auto&val)

{

if(!isFound&&property==&val)

isFound=true;

elseif(!isFound)

index++;

});

fieldName=FieldNames[index];

相当于使用Visitor遍历这个对象,找到对应成员的序号;



6.4问题



之后的版本可能考虑:



支持更多的SQL操作(如跨表);

改用语法树实现;(欢迎PullRequest??)

献花(0)
+1
(本文系thedust79首藏)