本文将简要记录一下本人作为一个菜鸟是如何构建出一个蹩脚ORM系统的过程。首先说一下背景,大概去年七月当时刚开始学习编程,学习了廖雪峰老师的Python教程。当时做了最后的实战但是很多东西并没有特别清楚,尤其是ORM(Object Relational Mapping),当时写的云里雾里,完全不能理解为什么要这样写。现在时间过了很久,当时是如何实现的也基本已经忘光了,于是借着自己做博客玩的机会尝试按照自己的思路来造了一次ORM,非常的蹩脚,但是做出来后很多当时不能理解的地方也能理解了,故记录一下这个过程。
使用的第三方库有MySQLdb
。
Database
先简要说一下database的设计,非常简单,三个表,分别对应user
, post
, 和comment
, 这里以post
为例。包含的columns有post_id
(PK), post_title
, post_context
以及其他一些并不那么重要的东西,这里就不一一列举了。
Class
然后开始撸代码,首先肯定是要有一个class来和post
表进行对应,于是一开始的代码就是这样的(省略了若干columns)
class Posts:
__table__ = "posts"
def __init__(self, title, context):
self.title = title
self.context = context
这里可以发现在init
中并没有传入post_id
,因为当时的想法是Primary Key是自增的,不应该由我来设置,就没有进行传入。嗯,自我感觉良好,继续开始撸。
Insert
有了class就要开始写方法了,首先想到的自然是如何插入一个项目到表中(我已然忘了database的术语应该如何描述这个东西。。。。)。最开始的代码就不放了,因为当时写好了之后很快就被我抹掉然后进行抽象了(当时想着这个insert应该其他的所有类都会有,应该抽象一下出来)。于是有了如下代码
def insert(self, db):
c = db.cursor()
sql = insert_sql(self)
try:
rows = c.execute(*sql)
except:
print(c._last_executed)
return False
if rows != 0:
db.commit()
c.close()
return True
return False
其中db
是全局变量,代表着连接的database
,c._last_executed
是最后执行的sql代码(因为一直是自己弄所以肯定会经常错,就用这个来看到底是哪里错了)。insert_sql
就是抽象出来的部分,用来生成sql代码。这个逻辑很简单就不详细说了,主要说一下insert_sql
的部分。
该如何实现insert_sql
呢,不管三七二十一先写一点是一点
def insert_sql(obj):
table = obj.__table__
然后就开始想了,我要怎么把各个columns
的名字一一对应上呢?是不是应该把class
的attribute
和columns
一一对应起来,然后获取每一个attributes
的名字,再进行插入?于是面向Google搜索了一下,得出结论,可以使用__dict__
来获得各个attributes
的名字。但是回头一看,发现其实自己class
各个attribute
的名字和数据表并不一一对应,于是做出了相应的改动。
def __init__(self, title, context):
self.post_title = title
self.post_context = context
def insert_sql(obj):
attrs = obj.__dict__
table = obj.__table__
attrs_name = ", ".join(list(attrs.keys()))
attrs_value = list(attrs.values())
h = "%s," * len(attrs_value)
sql = ("INSERT INTO {} ({}) values ({})".format(table, attrs_name, h[:-1]), (*attrs_value,))
return sql
从这里开始就感觉不是很好了,每一个属性都要一一对应,感觉是不是耦合度有点高?但是回头想一想感觉似乎也没有别的办法了?然后回头看了一下廖雪峰课程中的代码,发现似乎也是要一一对应的,就先放下了。
(这里有一个小插曲,在我写完第一个版本的insert_sql
之后(和这个不一样),我尝试了插入markdown
文档,然后发现完全乱七八糟的,各种转义都没有,然后发现我的sql语句生成使用的是format
来生成一个string
,而MySQLdb
的execute
其实是支持更加安全的转义的,是通过在string
中加入%
号和传入另外一个参数,这个参数为一个tuple
,和每一个%
一一对应,于是做出了更改。所以在这里返回的sql其实是一个tuple
而返回之后的执行也是通过*
来进行结构之后传入到execute
)
Select
insert
告一段落开始写select
,刚开始没多久我就发现似乎有点怪。。我怎么做到在已经初始一个实例之后再来进行选择呢。。回想了一下想起来class
都有一个装饰器@classmethod
来将一个方法转变为这个类的方法。Google了一下确认(真是开局一个def,剩下全靠Google),然后开始继续开心地撸码。
@classmethod
def select(cls, db, limit=None, where=None, order=None, desc=True):
c = db.cursor(MySQLdb.cursors.DictCursor)
sql = select_sql(cls, limit, where, order, desc)
rows = c.execute(sql)
if rows == 0:
return False
res = c.fetchall()
ret = []
for p in res:
post = cls(p["post_title"], p["post_context"], p["post_date"], p["post_abstract"], p["post_comments"], p["post_viewed"], p["post_id"])
ret.append(post)
return ret
这里就很明显能看到这个方法的问题了,最明显的就是我得一个个attribute
输进去,怎么能忍!!!不过我还是忍了(懒。。)。如果就这样写的话甚至都无法抽象出来放到Post
的父类中去作为方法继承下来。要抽象出来的话其实很麻烦,能想到的办法是多加一层for
循环把每一个p
中的属性取出来存到一个list
中再传到实例构建中,但是p
作为一个字典他是无序的,所以没有办法确保生成的顺序,想来想去还是改metaclass
,也就是廖雪峰老师的办法,因为之前已经写过就并没有再改了(懒。。。)。(临时写着又想了一个办法,就是在实例生成的时候直接传入一个dict
,然后生成的时候根据dict
的属性来一一生成,但是这样似乎并不是很安全)。
当时想着也不是不能用就继续了,select_sql
的实现也和insert_sql
差不多
def select_sql(obj, limit=None, where=None, order=None, desc=True, columns = "*"):
table = obj.__table__
sql = "SELECT {} FROM {}".format(columns, table)
if limit:
sql += " limit {}".format(limit)
if where:
sql += " where {}".format(where)
if order:
sql += " order by {}".format(order)
if desc:
sql += " desc"
sql += ";"
return sql
(可以发现我虽然设置了columns
但是并没有使用他。。真是懒癌晚期,这样就导致系统在抓取数据的时候速度会降低。不过其实当时没有使用columns
来进行选择的另外一个原因是如果我不全部抓取我就无法生成实例,不过后来想想,我并没有必要传回一个实例,现在再想想,如果我是通过一个dict
作为__init__
的参数进行传入,也同样不用担心这个问题)
总结一下吧
简单而言就是这样了,剩下的update
,delete
也都是大同小异,就不详细说了(我才不会说其实我delete
并没有做)。
一路写下来的感受就是不适用metaclass
确实是非常的不灵活,诚如董明伟所言 ,这样的操作非常不优雅(都要写吐了)。