如何设计一个简单的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??)
|
|