
在使用數(shù)據(jù)庫的正確姿勢(shì)(一)中,我給大家介紹了數(shù)據(jù)庫應(yīng)用的第一個(gè)層次——數(shù)據(jù)庫的基本使用,下面我將給大家介紹剩下兩個(gè)層次,分別是
- 面向?qū)ο蟮臄?shù)據(jù)庫思維
- 使用分布式數(shù)據(jù)庫
面向?qū)ο蟮臄?shù)據(jù)庫思維
What
你可能會(huì)問,什么是面向?qū)ο蟮臄?shù)據(jù)庫思維?
我對(duì)這個(gè)概念的了解,源于MVC框架中的M——Model,記得很多年前,我做過MFC的開發(fā),當(dāng)時(shí),MFC的出現(xiàn)把界面開發(fā)變得非常簡單:要完成一個(gè)項(xiàng)目,你可以不了解操作系統(tǒng),不了解數(shù)據(jù)庫,只需要專注你的業(yè)務(wù)邏輯即可——它解決了程序員的痛點(diǎn),于是程序員寫出來的代碼變得“又快又好”。
同時(shí),那也是我做過的第一個(gè)使用了數(shù)據(jù)庫,卻沒有調(diào)用任何一行SQL語句的項(xiàng)目,要讀寫數(shù)據(jù)庫,你只需要操作Model中的對(duì)象即可。
那次項(xiàng)目后,我再也沒有這樣使用過數(shù)據(jù)庫,直到最近學(xué)rails才讓我回想起來,例如,以下兩行代碼做的是同一件事情(如果你沒有見過這種語法,你可以忽略它給你帶來的不適,把焦點(diǎn)放在代碼的形式上),可以看到,第一行代碼是對(duì)sql查詢語句進(jìn)行直接調(diào)用,而第二行代碼就像是在使用一個(gè)對(duì)象;不知道你對(duì)比這兩行代碼會(huì)有什么感受?
sql.execute("select * from group where id=10;")
@group = Group.find(10)
對(duì)于我來說,這種操作方式不但喚醒了我曾經(jīng)學(xué)習(xí)MFC的經(jīng)驗(yàn),同時(shí)我也想知道其他編程語言有沒有類似的方法,不出意料之外,搜索之后發(fā)現(xiàn)幾乎所有的編程語言,在數(shù)據(jù)庫方面都有類似的代碼庫,同時(shí)業(yè)界也給“面向?qū)ο蟮臄?shù)據(jù)庫思維”起了個(gè)專有的名字——ORM(Object-relational mappers)
ORM是一個(gè)自動(dòng)將關(guān)系型數(shù)據(jù)庫中的數(shù)據(jù)和程序中的對(duì)象互相關(guān)聯(lián),并可以在二者間交換數(shù)據(jù)的代碼庫
這張圖可以很形象的對(duì)這個(gè)概念進(jìn)行解釋,左邊的是存儲(chǔ)在數(shù)據(jù)庫中的記錄,右邊是Python的對(duì)象,而ORM則是兩者之間的橋梁:

Why
這時(shí)你可能又會(huì)問了,ORM有什么用呢?難道就只是在敲代碼的時(shí)候可以少敲幾個(gè)字符嗎?我們?yōu)槭裁匆盟兀?/p>
從工程的角度,技術(shù)經(jīng)理為了保證項(xiàng)目能夠快速開發(fā),便于維護(hù),一般會(huì)說這樣的話:讓程序員專注于業(yè)務(wù)邏輯部分,把其他的部分交給框架來完成。
因?yàn)榘褬I(yè)務(wù)邏輯獨(dú)立于框架范圍之外,實(shí)現(xiàn)邏輯部分高內(nèi)聚,及與框架部分的低耦合,可以讓項(xiàng)目更為可控。
具體來說,如果你不這樣做,你的數(shù)據(jù)庫操作部分的代碼可能是這樣的,我用偽代碼表示如下:
// 第一步、確保與數(shù)據(jù)庫的連接可用,如果不可用,需要重連
if sql.is_connect == False:
sql.connect()
// 第二步、組裝sql語句
cmd = "select * from user where userid = %d" % userID
// 第三步、執(zhí)行sql語句
result = sql.execute(cmd)
// 第四步、解析返回結(jié)果
for _r in result:
print _r
為了讓代碼更為簡潔,功能更加單一,我們會(huì)將這部分代碼定義為一個(gè)函數(shù)(例如findUserByID),可以預(yù)見的是,我們每寫一個(gè)SQL語句,都需要定義一個(gè)這樣的函數(shù),久而久之,你會(huì)發(fā)現(xiàn)你已經(jīng)寫了一堆這樣的函數(shù),同時(shí)這些函數(shù)中有很多重復(fù)的代碼,你想對(duì)他們進(jìn)行抽象,卻又發(fā)現(xiàn)它們操作的都是不同的數(shù)據(jù)結(jié)構(gòu),最終只能無奈的放棄。
如果你只負(fù)責(zé)開發(fā)一個(gè)這樣的項(xiàng)目,也許你還可以忍,但如果你是一個(gè)項(xiàng)目管理者,你有10個(gè)項(xiàng)目,每個(gè)項(xiàng)目都有一堆這樣的數(shù)據(jù)庫代碼,你還敢說你的程序有很好的復(fù)用性嗎?
《程序員修煉之道》里講了一個(gè)破窗理論:
如果一個(gè)房子破了一扇窗戶而不及時(shí)修理好,那么很快這間房子的其他窗戶也會(huì)被破壞掉。
這個(gè)理論適用于項(xiàng)目中的很多方面,例如編碼規(guī)范、單元測(cè)試,以及我們這里所提到的DRY(Don't Repeat Yourself)原則:
如果你的代碼中有一處看上去使用了“復(fù)制粘貼技術(shù)”,那么很快其他地方也會(huì)使用“同樣的技術(shù)”。
所以如果你希望你的團(tuán)隊(duì)遵守DRY原則,你可以考慮使用ORM
代碼復(fù)用能力強(qiáng),意味著開發(fā)效率高,也就意味著你的代碼是可控的,例如你可以把數(shù)據(jù)庫的操作都控制在ORM的調(diào)用中,以后要對(duì)數(shù)據(jù)庫進(jìn)行擴(kuò)展,你只需要修改對(duì)應(yīng)的代碼即可——這部分代碼一般被我們稱為Model模塊。
除了開發(fā)效率和擴(kuò)展性之外,ORM還有哪些其他的優(yōu)勢(shì)呢?讓我們考慮下面一種情況,當(dāng)我需要組裝一個(gè)SQL語句時(shí),我通常需要從當(dāng)前的開發(fā)語言切換到SQL語言,在切換的過程中,我很可能會(huì)突然忘記這條SQL語句怎么寫,例如我經(jīng)常忘記Update的具體語法,于是我便會(huì)Google一下,這個(gè)動(dòng)作明顯會(huì)打斷我寫代碼的思路。這是其一,其二是當(dāng)你讀別人的代碼時(shí),到了數(shù)據(jù)庫部分,看到又長又復(fù)雜的SQL語句,會(huì)不會(huì)有一種不順暢的感覺呢?不管你有沒有,反正我是有的。
所以,使用ORM的第二個(gè)優(yōu)勢(shì)是;它讓你的代碼保持單一的語法,增強(qiáng)了可讀性。
話說回來,和其他框架一樣,ORM也有缺點(diǎn)——它會(huì)帶來性能上的損耗。但我認(rèn)為在互聯(lián)網(wǎng)高速發(fā)展的今天,在應(yīng)用沒有大到需要你優(yōu)化它之前,犧牲掉一定的性能來換取工作效率的提升是非常值得的。
所以,你現(xiàn)在知道為什么Rails、Flask之類的框架都使用ORM了吧,即便是這樣,很多大流量的項(xiàng)目,仍然會(huì)使用這些框架進(jìn)行開發(fā),例如github網(wǎng)站不就是用的Rails開發(fā)的嗎,難道他們就不關(guān)心性能嗎?當(dāng)然關(guān)心,只是因?yàn)樾收娴氖翘匾?,因?yàn)樾矢撸栽诹髁可蟻碇?,是一定有辦法解決掉性能問題的。
下圖是Python框架中,F(xiàn)lask和Django所使用的ORM,可以看到,SQLAlchemy和MySQL-python就是我們之前使用過的代碼庫。

關(guān)于SQLAlchemy,它還有以下幾個(gè)特點(diǎn)
- 可以自動(dòng)同步model和元數(shù)據(jù),當(dāng)你在model中增加了一個(gè)字段,會(huì)自動(dòng)反應(yīng)在數(shù)據(jù)表的元數(shù)據(jù)中,不過需要借助Alembic插件來完成。
- 與web框架無縫結(jié)合:對(duì)于每個(gè)web請(qǐng)求,會(huì)產(chǎn)生一個(gè)數(shù)據(jù)庫連接,每個(gè)請(qǐng)求被封裝到一個(gè)transaction中,其中的錯(cuò)誤機(jī)制也非常完善,可以讓你的項(xiàng)目更為健壯
- Eager loading:這個(gè)特性會(huì)將多個(gè)查詢語句合并成一條,從而優(yōu)化性能。例如下面的代碼會(huì)產(chǎn)生1+n條查詢語句,1代表對(duì)item表查詢1次,n代表循環(huán)中對(duì)product表的查詢次數(shù),這會(huì)造成大量的查詢而影響性能
for item in order.items:
print(item.product.name, item.quantity)
所以我們可以提前告訴SQLAlchemy,讓它一次性把item全部load出來,減少查詢次數(shù),類似下面的語法。
session.query(Order).options(joinedload_all('items.product')).get(order_id)
- 提供鉤子,用戶可以重載庫中的方法,進(jìn)而做到定制化開發(fā)
- 默認(rèn)的id字段:對(duì)于新創(chuàng)建的表,會(huì)默認(rèn)分配id字段,對(duì)于已存在的表,可以使用sqlacodegen工具,將舊表倒入到新表
- 完善的文章:一般開源項(xiàng)目最大的缺點(diǎn)是文檔不完善,而該項(xiàng)目的文檔介紹了從入門到精通的各個(gè)方面。
How
下面我們來看一下ORM的CRUD
- Create
這里是新增一條記錄的部分代碼
meta, engine = get_meta()
session = get_session(engine)
User = get_table("user", meta, engine)
try:
row = User(name="Jerry", age=18)
session.add(row)
for res in session.query(User).all():
print "add success: name(%s), age(%d)" % (res.name, res.age)
session.commit()
except Exception as e:
session.rollback()
finally:
session.close()
其中,session代表一個(gè)數(shù)據(jù)庫連接,它也是SQLAlchemy中非常重要的概念,對(duì)于今后要使用該庫的同學(xué),建議閱讀一下官方文檔
get_table("user", meta, engine)表示在指定的數(shù)據(jù)庫中獲得一張名為user的數(shù)據(jù)表。
session.add(row)表示增加一行數(shù)據(jù),而session.rollback和session.commit分別表示事務(wù)的回滾和提交
- Read
接著我們來看Read的代碼
query = session.query(User).filter(User.name == 'Jerry')
for _row in query.all():
print "query success: name(%s), age(%d)" % (res.name, res.age)
query功能非常強(qiáng)大,其中filter相當(dāng)于SQL語句中的where,更多的條件查詢示例可以參考這個(gè)鏈接
- Update
try:
row = session.query(User).filter(User.name == 'Jerry').first()
row.age = 19
session.commit()
except Exception as e:
session.rollback()
finally:
row = session.query(User).filter(User.name == 'Jerry').first()
print "update success: name(%s), age(%d)" %(row.name, row.age)
update的寫法更為自然,更新操作就是賦值操作,賦值后,接著調(diào)用commit()才會(huì)寫到數(shù)據(jù)庫中
- Delete
query = session.query(User).filter(User.name=='Jerry')
query.delete()
session.commit()
query = session.query(User).filter(User.name=='Jerry')
print "delete success: %s" % query.all()
刪除操作也相當(dāng)簡單,操作完畢后,所有name=='Jerry'的記錄將被清空,在這里透露一個(gè)小秘密,一般互聯(lián)網(wǎng)是很少刪數(shù)據(jù)的,即便用戶主動(dòng)刪除,可能也只是在記錄中做一個(gè)“已刪除”標(biāo)記,原因在于:1)刪除數(shù)據(jù)影響性能;2)相對(duì)于用戶數(shù)據(jù)來說,硬盤更為廉價(jià)
運(yùn)行后的輸出如下:
$ python object_relational_crud.py
add success: name(Jerry), age(18)
query success: name(Jerry), age(18)
update success: name(Jerry), age(19)
delete success: []
以上便是ORM的基本使用,其他更多高級(jí)用法,比如聯(lián)表查詢、用戶自定義查詢等,還需要你自己去探索。
分布式數(shù)據(jù)庫應(yīng)用
最后我將給大家介紹一下數(shù)據(jù)庫使用的第三個(gè)層次,使用分布式數(shù)據(jù)庫,那么我們什么時(shí)候需要使用分布式數(shù)據(jù)庫呢?如何把數(shù)據(jù)庫調(diào)整為分布式呢?且如果調(diào)整了分布式之后,我們的程序需要如何修改呢?
其實(shí)這幾個(gè)問題都很簡單,當(dāng)我們的數(shù)據(jù)量大到一定程度,或者預(yù)估我們的產(chǎn)品將來會(huì)有千萬級(jí)別的用戶量,那么我們就需要使用分布式數(shù)據(jù)庫,也就是我們常說的對(duì)數(shù)據(jù)庫進(jìn)行分表分庫,對(duì)于MySQL來說,一張表的極限大概在百萬級(jí),所以就很容易預(yù)估出上億級(jí)的數(shù)據(jù)量,只用分100張表即可滿足需求。
如果不是在設(shè)計(jì)過程中就把分表的問題考慮好了,那么我們?cè)诜直碇筮€需要做數(shù)據(jù)的遷移工作,所以為了減少DBA的負(fù)擔(dān),最好在設(shè)計(jì)之初就考慮好:哪些業(yè)務(wù)需要分表,哪些不需要。
前面兩個(gè)問題解決之后,我們的程序需要做怎樣的調(diào)整呢?
一方面:為了保證數(shù)據(jù)的唯一性,我們需要將同一個(gè)用戶的數(shù)據(jù)存儲(chǔ)在同一張表中,此時(shí)便需要在訪問具體的數(shù)據(jù)之前,先對(duì)數(shù)據(jù)的key進(jìn)行hash取模操作,假設(shè)我們分100張表,hash函數(shù)為crc32,則我們可以通過這個(gè)公式來計(jì)算用戶的數(shù)據(jù)具體存儲(chǔ)在哪張表中:
index = crc32(key) % 100
另一方面,我們還需要一個(gè)動(dòng)態(tài)獲取表的方法,還記得剛才提到的get_table函數(shù)嗎?不記得也沒關(guān)系,下面我們來看一下這個(gè)函數(shù)

這個(gè)函數(shù)的功能是,返回一個(gè)表名為name的表對(duì)象,name為上一步算出來的表名;其次,如果這個(gè)表在數(shù)據(jù)庫中不存在,會(huì)先創(chuàng)建這張表。
總結(jié)上面兩個(gè)步驟,要讓程序能適應(yīng)于分布式數(shù)據(jù)庫,對(duì)于ORM而言,需要先計(jì)算數(shù)據(jù)對(duì)應(yīng)的表名,然后動(dòng)態(tài)獲取表對(duì)象
所以,使用了ORM之后,切換到分布式數(shù)據(jù)庫,對(duì)代碼的改動(dòng)是非常小的。
以上便是分表分庫具體的操作思路,你也許會(huì)覺得分布式數(shù)據(jù)庫過于簡單。實(shí)際上并非如此,分布式最難的地方不在這里,而在于ACID,由于篇幅原因,就不在這里展開了,讀者有興趣可以繼續(xù)深入研究。
下面我們來總結(jié)一下,今天我給大家介紹了數(shù)據(jù)庫使用的三個(gè)方面,分別是:
- 數(shù)據(jù)庫的基本使用
- 利用面向?qū)ο蟮乃季S使用數(shù)據(jù)庫
- 在項(xiàng)目中應(yīng)用分布式數(shù)據(jù)庫
上面演示的代碼可以從github下載,大家可以親自動(dòng)手體驗(yàn)一下。
參考資料:
http://pajhome.org.uk/blog/10_reasons_to_love_sqlalchemy.html
https://www.fullstackpython.com/object-relational-mappers-orms.html
https://github.com/crazyguitar/pysheeet/blob/master/docs/notes/python-sqlalchemy.rst