通过抽象Sql语句来构建流畅的数据访问

如果用数据库做持久化设备的话直接写sql(存储过程)还是用ORM呢?
无疑ORM的出现给了我们一个全新的看待数据操作的视角:在面向对象的语言中,用面向对象的方法访问,操作数据
但是在使用ORM一段时间后,总是会觉得有点别扭的地方,因为关系型数据库对数据的抽象方式是基于关系代数的层次结构,而对象化的数据抽象方式是网状结构,ORM在这两种抽象模式之间映射就会产生Impedance Mismatch(阻抗不匹配)的现象,所以在映射不当的时候就会感觉非常的别扭。
那么我再想,如果抽象数据本身存在阻抗不匹配的问题,那么如果我们抽象对数据的访问呢?无论如何ORM,对关系数据库的访问仍然是通过SQL语句作为访问的界面,那么如果我们抽象SQL语句是否就能摆脱阻抗失调,能够流畅的访问操作数据了呢?我在上一篇文章发布的一个Python的DAL里做了一个小小的尝试,当然我并不是第一个这么做的人,不过我想把这种方法总结出来,也许能够在ORM以外提供一个更新的视角,能够产生出更加轻量化的数据访问组件,也是不错的。用Python实现是因为暂时我还只想到了在动态语言下如何实现,如果需要在.NET下使用,可以用IronPython。如果有同学想到了在C#下如何实现也希望能够拿出来和大家分享一下。
为什么要用动态语言来实现,首先就是不要实体类,因为我们抽象的是访问数据的方式而不是数据本身,在关系型数据库里数据就是数据行,字段的形式,在数据结构上一行数据用字典就能够表示了,而在python下我们可以把字典用下面的形式来表现:
class Row(dict):
    def __getattr__(self,propertyName):
        if self.has_key(propertyName):
            return self[propertyName]
        return None

这样一个Row的对象我们就可以通过列明对应属性来访问其中的键值,就跟一个实体类一样,这样我们就解决了数据抽象的问题。
接下来是重点,我们要抽象SQL,首先要分析SQL的结构,我们对数据的操作有 增、删、改、查四类,分别通过:
INSERT、DELETE、UPDATE、SELECT四种SQL语句来实现,每一种SQL语句又由子句构成,四种SQL语句的子句中有各自独有到,也有共有的子句,比如WHERE子句,SELECT、UPDATE、DELETE 这三种SQL操作都有WHERE子句,且语义相同,均表示待操作的数据筛选到条件,所以我将筛选条件抽象出来定义了conds这个类。
class conds:
    def __init__(self,field):
        self.field_name=field
        self._sql=""
        self._params=[]
        self._has_value=False
        self._sub_conds=[]
        self._no_value=False
这个类用来将语言的关系运算逻辑映射到SQL语句上,所幸的是Python支持操作符重载,所以我们只需要在conds类中加入相应的buildin方法就行了。比如:
def __eq__(self,value):
    return self._prepare("".join(["`",self.field_name,"`=%s"]),value)

def _prepare(self,sql,value):
    if not self._has_value:
        self._sql=sql
        self._params.append(value)
        self._has_value=True
        return self
    raise OperationalError,"Multiple Operate conditions"

上面代码就定义了如果 conds('col1')==5 就会映射到sql col1=%s 且增加一个参数,值为5。
同理将其他关系操作符的方法都映射到相应的buildin方法中,两个关系间是and和or,我们用__and__和__or__这两个方法来映射,还有Like找不到相应的操作符,就用like方法来表示这个关系,条件操作的结果还是条件,所以要用对象本身作为返回值。这样 (conds('col1')==5)&(conds('col2')<6) 就映射到了条件 col1=%s and col2<%s这两个条件的并集。由于这么写还需要用字符串来表示列名不方便,所以我们定义一个table类来获取conds:
class TableQueryer:
    '''
    Support for single table simple querys
    '''
    def __init__(self,db,tablename):
        self.tablename=tablename
        self.db=db

    def __call__(self,query=None):
        return Operater(self.db,self.tablename,query)

    def __getattr__(self,field_name):
        return conds(field_name)

同理我们在数据连接类上加入getattr,
def __getattr__(self,tablename):
    '''
    return single table queryer for select table
    '''
    return TableQueryer(self,tablename)
    
然后我们就能够通过db.tablename.colname来获取条件对象了,之前的例子就能改成
(db.tablename.col1==5)&(db.tablename.col2<6)
如果预先 tb=db.tablename 的话还可以改写成
(tb.col1==t)&(tb.col2<6)

由于插入数据只需要数据表名不需要查询为基础,所以直接在TableQueryer中加入insert方法
所以插入数据就只需要db.tablename.insert(col1=value1,col2=value2...)

数据的查询、更新、删除都是基于查询条件的,所以抽象出一个操作对象Operate出来:
class Operater:
    def __init__(self,db,tablename,query):
        self.count=Count(db,tablename,query)
        self.select=Select(db,tablename,query)
        self.update=Update(db,tablename,query)
        self.delete=Delete(db,tablename,query)

每一个操作抽象一个类出来,并且把查询条件保存到了每一个操作的条件里,最终的Sql生成,以及特定子句的加入,都在具体的类比如Select里完成。
我在这里为了节约一个方法名的层次出来就把TableQueryer做成了可以调用的对象,加入了__call__方法。将查询条件传入__call__方法就返回了一个Operator对象,于是通过
tb=db.tablename
q=tb((tb.col1==5)&(tb.col2<6))
q.select()就能查询出结果,等价sql为 select * from tablename where col1=5 and col2<6
在得到q后,直接调用q.delete()就能把这些数据删掉
q.update(tb.col1==6)就能更新筛选出的数据
q.delete()就能直接删掉符合筛选条件的数据

项目放在 http://bitbucket.org/alexander_lee/flunt-sql-data-access-layer
有兴趣的TX一起研究研究






posted on 2010-08-25 20:42 亚历山大同志 阅读(2475) 评论(25) 编辑 收藏

评论

#1楼 2010-08-25 20:57 henry      

一直使用的方式

(Category.iD == id).Delete<Category>();
(Category.iD == id).Edit<Category>(o=>{o.Remark="test";});
 回复 引用 查看   

#2楼[楼主] 2010-08-25 21:08 亚历山大同志      

@henry
有一点点区别,呵呵
 回复 引用 查看   

#3楼 2010-08-25 22:08 老鸵      

这种方式和一些orm里的ObjectQuery对象区别不大,就是动态的构造sql语句吧。C#也可以很容易实现的。  回复 引用 查看   

#4楼 2010-08-25 23:06 MKing's Kindom      

用cs写过类似的东西,做一个基类DataElement,里面放一个字典<string, DataItem>。string是列名,DataItem是自定义的列对象,存储列的数据类型、长度、是否是索引、是否FK\是否pk等等,然后用这个字典来构造语句,在基类中实现四个SQL命令。开放一个IDbConnection属性来实现连接数据库。然后每个数据表建一个对应的类继承这个基类,只要在表类里定义这个字典和表名就可以了。

但是后来没法用了,简单的语句还行,JOIN、UNION、分页了,等等这些就比较麻烦,有时候我需要join3个表,sql语句有500多字符。其实我觉得要完全实现还是得依赖存储过程或者表函数之类的数据库端的东西,纯粹sql语句效率且不说哈,单sql语句分析就够喝一壶的。我在我那个dataelement里实现了JOIN,但是烂得我自己都不想再去看它。我就是那种和蔼的平庸程序员,只能到这个程度了。
 回复 引用 查看   

#5楼 2010-08-26 01:31 Ivony...      

感觉其实就是在重复造LINQ的轮子。

SQL并不是一个很好的切入点,因为SQL本身就是一个问题重重的语法(七拼八凑加上先天理论缺陷)。如果要抽象,不如直接从数学的角度切入(就像LINQ Expression,除了长得像SQL其实没半毛钱关系)。

而SQL本身就包含很多种,用于查询的SELECT和用于操纵的IDU混为一谈也不是好事。譬如说IDU就不可能有GROUP或是LIMIT(TOP)之类的语法。
 回复 引用 查看   

#6楼[楼主] 2010-08-26 09:37 亚历山大同志      

@Ivony...
一个实际的问题就是无论如何去切入,你实质上是绕不开SQL的,这是起一,SQL本身问题重重但是抽象的本身作用就是在这个问题重重的东西上面盖一层漂亮的界面来隐藏内部肮脏的本质,关系型数据库的访问离不开SQL,所以Linq再怎么和SQL没有半毛钱关系也得toSQL了才能操作数据库,并且实现得并不怎么漂亮,那个IDE生成的实体类我就不说了,
所以说实质上我这个东西就是给SQL提供了一个可局部复用(复用查询的筛选数据部分),根据28原则,把常用的大部分单表的简单查询用编程的API封装起来(不用字符串),然后剩下的复杂查询可以直接写SQL或者存储过程,一开始我就没强求什么都包含进去,这个不现实

 回复 引用 查看   

#7楼[楼主] 2010-08-26 09:38 亚历山大同志      

@MKing's Kindom
你多半没仔细看就回复了,我之针对单表和简单的自查寻提供了接口,而复杂查询不支持,你可以自己写sql或者存储过程
 回复 引用 查看   

#8楼 2010-08-26 09:52 玉开      

你的想法和官方的linq to sql或者dataentity framework有什么区别呢?  回复 引用 查看   

#9楼 2010-08-26 10:08 MKing's Kindom      

@亚历山大同志
不是,是python代码看不懂哈
 回复 引用 查看   

#10楼 2010-08-26 10:18 Kain      

引用亚历山大同志:
@Ivony...
一个实际的问题就是无论如何去切入,你实质上是绕不开SQL的,这是起一,SQL本身问题重重但是抽象的本身作用就是在这个问题重重的东西上面盖一层漂亮的界面来隐藏内部肮脏的本质,关系型数据库的访问离不开SQL,所以Linq再怎么和SQL没有半毛钱关系也得toSQL了才能操作数据库,并且实现得并不怎么漂亮,那个IDE生成的实体类我就不说了,
所以说实质上我这个东西就是给SQL提供了一个可局部复用(复用查询的筛选数据部分),根据28原则,把常用的大部分单表的简单查询用编程的API封装起来(不用字符串),然后剩下的复杂查询可以直接写SQL或者存储过程,一开始我就没强求什么都包含进去,这个不现实


Linq 是Linq 借用表达式我们可以将其翻译成任何东西,不单单是Sql而已  回复 引用 查看   

#11楼 2010-08-26 10:19 MKing's Kindom      

我又看了一遍哈,我再确认一下。
我做了一个数据表对象Article,对应表article。
Article a = new Article(iconn);
a.id=100;
a.select();
return a["Title"];
另外我所说的使用存储过程也是整合到这个对象里的,比如
Resource r = new Resource(iconn);
a.id=100;
DataElement de = a.join(r).select();
这里其实是在后台执行了sql语句EXEC ArticleJoinResource 100
我这个数据表对象和你说的不是一个事吗?
 回复 引用 查看   

#12楼 2010-08-26 10:39 深蓝医生      

不知道PDF.NET数据开发框架的OQL算不算对SQL的抽象,下面举一个例子:
1,查询:
User u = new User();
OQL q = new OQL(u);
q.Select().Where(q.Condition.IN(u.Uid, new object []{1,3,5}));
//OQL将映射成下面的SQL:
SELECT *
FROM Tb_User
Where id IN(1,3,5)

//继续执行
q.Select().Where(q.Condition.IN(u.Name, new object[] { '张三','李四'}));
//OQL将映射成下面的SQL:
SELECT *
FROM Tb_User
Where id IN(1,3,5) AND Name IN('张三','李四')
2,排序:
q.Select().Where(q.Condition.IN(u.Uid, new object []{1,3,5})).OrderBy (u.Name ,'desc');
//OQL将映射成下面的SQL:
SELECT *
FROM Tb_User
Where id IN(1,3,5)
Order by Name desc
3,统计:
q.Select().Count(p.FundCode, "CountField1").GroupBy (p.BankCode ).OrderBy (p.BankCode,"desc" );
Console.WriteLine("SQL=" + q.ToString());
Console.Read();
//OQL将映射成下面的SQL:
SQL=SELECT BankCode,COUNT(FundCode) AS CountField1
FROM PFT_FundReviews
Group By BankCode
Order by BankCode desc
 回复 引用 查看   

#13楼[楼主] 2010-08-26 11:09 亚历山大同志      

@Kain
你不翻译成SQL能够访问关系型数据库么?
 回复 引用 查看   

#14楼[楼主] 2010-08-26 11:11 亚历山大同志      

@深蓝医生
建议将Where放在Select的前面,既然Update,delete也需要Where子句,为什么不能复用呢?
 回复 引用 查看   

#15楼[楼主] 2010-08-26 11:13 亚历山大同志      

@MKing's Kindom
根据对象生成SQL和针对Sql抽象最后都是生成SQL,但是不要把生成SQL并操作数据库都看成一码事
 回复 引用 查看   

#16楼 2010-08-26 11:19 Kain      

引用亚历山大同志:
@Kain
你不翻译成SQL能够访问关系型数据库么?


生成Sql应该不是目的。Linq给了一个比较好的思路,将查询和最终的实现分离开来。你现在用抽象的Sql对象来描述T-sql感觉是一种退化。  回复 引用 查看   

#17楼[楼主] 2010-08-26 11:49 亚历山大同志      

@Kain
拜托不要老是感觉好不拉
Linq是不错,我没说Linq如何如何,切入点不一样,我没有Linq那么大的野心,提出一个DSL几乎相当于另外提供一门需要学习的语言了,你先搞清楚这东西的应用场景再下结论嘛。
就用LinqToSql来说,当然也只能用这个来比较,LinqToSql人家名字都说明得很清楚了,就是要生成SQL语句的嘛
 回复 引用 查看   

#18楼 2010-08-26 12:25 深蓝医生      

引用亚历山大同志:
@深蓝医生
建议将Where放在Select的前面,既然Update,delete也需要Where子句,为什么不能复用呢?

主要是为了尽量模拟SQL的语法结构,下面是OQL的Update语法:
PFT_FundReviews p = new PFT_FundReviews();
OQL q = new OQL(p);
p.BankCode = "20";
p.FundCode = "KF001";
p.CityCode = "0210";
q.Update( p.CityCode,p.BankCode ).Where (p.FundCode );
//OQL 将映射成下面的SQL:
Update PFT_FundReviews set CityCode=@CityCode,BankCode=@BankCode Where FundCode=@FundCode
//参数的值将从实体类属性获取  回复 引用 查看   

#19楼 2010-08-26 13:01 金色海洋(jyk)      

我觉得可以从另一个角度来考虑。

SQL是什么?对于关系型数据库来说,是指令、编程语言。
但是对于C#这类的语言来说,只不过是一堆字符串。

不管怎么弄,其实就是一个如何“拼接字符串(SQL)”的问题。

另一个问题就是解决便于维护、可读性和数据库结构变了程序部分不用变。

这两个问题解决了就OK了。
 回复 引用 查看   

#20楼 2010-08-26 13:20 Ivony...      

引用亚历山大同志:
@Ivony...
一个实际的问题就是无论如何去切入,你实质上是绕不开SQL的,这是起一,SQL本身问题重重但是抽象的本身作用就是在这个问题重重的东西上面盖一层漂亮的界面来隐藏内部肮脏的本质,关系型数据库的访问离不开SQL,所以Linq再怎么和SQL没有半毛钱关系也得toSQL了才能操作数据库,并且实现得并不怎么漂亮,那个IDE生成的实体类我就不说了,
所以说实质上我这个东西就是给SQL提供了一个可局部复用(复用查询的筛选数据部分),根据28原则,把常用的大部分单表的简单查询用编程的API封装起来(不用字符串),然后剩下的复杂查询可以直接写SQL或者存储过程,一开始我就没强求什么都包含进去,这个不现实


你说的很对。。。。  回复 引用 查看   

#21楼 2010-08-26 13:34 Linq.C#      

目前为止 我始终认为 只要类似LINQ这样的查询方式的ORM框架实现才是最好用,最易用的..(如果不考虑那一点性能影响)

 回复 引用 查看   

#22楼[楼主] 2010-08-26 13:42 亚历山大同志      

@Linq.C#
对C#来说是,python表示无压力,因为即使不用动态特征也一样不会变快,那还不大用特用,呵呵
 回复 引用 查看   

#23楼 2010-08-26 13:46 MKing's Kindom      

引用亚历山大同志:
@MKing's Kindom
根据对象生成SQL和针对Sql抽象最后都是生成SQL,但是不要把生成SQL并操作数据库都看成一码事

又看了一遍你的文章和别人的回复,的确没看仔细,顺便补了补orm。不要实体类、字典、中间件、LinqToSql……希望我真明白你想干什么了。
我现在依然是基于.net2做开发,Ling什么的一直没有接触,只能自己琢磨些使数据对象化的方法,只是没有ORM的概念,整体设计可能有些混乱。写一点代码意思一下我的做法:
abstract class DataElement{
protected Dictionary<string,DataItem> dic;
protected string tableName;
public DataElement(string tableName){
this.tableName = tableName;
this.dic = new Dictionary<string,DataItem>;
}

protected virtual string insertString(){
foreach(string key in this.dic.Keys){
....
}
return String.Format("INSERT ")
}
public void Insert(IDbConnection conn){
using(IDbCommand cmd = conn.CreateCommand()){
cmd.CommandText = this.insertString();
....
cmd.exec;
}
}
protected virtual string selectString(){
foreach(string key in this.dic.Keys){
....
}
return String.Format("SELECT....WHERE... ")
}
public void Select(IDbConnection conn){
DataTable dt;
using(IDbCommand cmd = conn.CreateCommand()){
cmd.CommandText = this.insertString();
....
IDataAdapter da = ....
da.Fill(dt);
....
foreach(DataColumn col in dt.Columns){
if(this.dic.ContainsKey(col.ColumnName){
this.dic[col.ColumnName].Value = dt.Rows[0][col];
}
}
}
}
}

class Article : DataElement{
public Article : base("Article"){
base.dic["Title"] = new DataItem("Title",DbType.String);
...
}
}

是不是应该把sql语句生成独立出来?如果这和你说的不是一个东西的话,我不知道怎么来定义我这种方法。我想要按照ORM的方法来重写它能给点建议吗?  回复 引用 查看   

#24楼 2010-08-26 13:47 MKing's Kindom      

e...缩进都没了。如果觉得看不清我写到博客里再发消息请你去看。  回复 引用 查看   

#25楼[楼主] 2010-08-26 14:47 亚历山大同志      

@MKing's Kindom
@henry的那个做的不错,.NET下你可以借鉴一下他的那个组件
 回复 引用 查看   

导航

公告


放一首适合飚车的音乐,听这个开车会不知不觉的加速
昵称:亚历山大同志
园龄:5年1个月
荣誉:推荐博客
粉丝:117
关注:0
<2010年8月>
25262728293031
1234567
891011121314
15161718192021
22232425262728
2930311234

统计

搜索

 
 

常用链接

最新随笔

我的标签

随笔分类(128)

随笔档案(134)

相册

朋友的Blog

同事的Blog

积分与排名

最新评论

阅读排行榜

评论排行榜

推荐排行榜