Python ORM 概覽
作為一個美妙的語言,Python 除了 SQLAlchemy 外還有很多ORM庫。在這篇文章裡,我們將來看看幾個流行的可選 ORM 庫,以此更好地窺探到Python ORM 境況。通過寫一段指令碼來讀寫2個表 ,person 和 address 到一個簡單的資料庫,我們能更好地理解每個ORM庫的優缺點。
SQLObject
SQLObject 是一個介於SQL資料庫和Python之間對映物件的Python ORM。得益於其類似於Ruby on Rails的ActiveRecord模式,在程式設計社群變得越來越流行。首個 SQLObject在2002年十月釋出。它遵循LGPL許可。
在 SQLObject 中,資料庫概念是通過與 SLQAlchemy 非常類似的的一種方式對映到Python的,表對映成類,行作為例項而欄位作為屬性。它同時提供一種基於Python物件的查詢語言,這使得 SQL 更加抽象, 從而為應用提供了資料庫不可知性(譯註:應用和資料庫分離)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
$ pip install sqlobject Downloading/unpacking sqlobject Downloading SQLObject-1.5.1.tar.gz (276kB): 276kB downloaded Running setup.py egg_info for package sqlobject warning: no files found matching '*.html' warning: no files found matching '*.css' warning: no files found matching 'docs/*.html' warning: no files found matching '*.py' under directory 'tests' Requirement already satisfied (use --upgrade to upgrade): FormEncode>=1.1.1 in /Users/xiaonuogantan/python2-workspace/lib/python2.7/site-packages (from sqlobject) Installing collected packages: sqlobject Running setup.py install for sqlobject changing mode of build/scripts-2.7/sqlobject-admin from 644 to 755 changing mode of build/scripts-2.7/sqlobject-convertOldURI from 644 to 755 warning: no files found matching '*.html' warning: no files found matching '*.css' warning: no files found matching 'docs/*.html' warning: no files found matching '*.py' under directory 'tests' changing mode of /Users/xiaonuogantan/python2-workspace/bin/sqlobject-admin to 755 changing mode of /Users/xiaonuogantan/python2-workspace/bin/sqlobject-convertOldURI to 755 Successfully installed sqlobject Cleaning up... |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
>>> from sqlobject import StringCol, SQLObject, ForeignKey, sqlhub, connectionForURI >>> sqlhub.processConnection = connectionForURI('sqlite:/:memory:') >>> >>> class Person(SQLObject): ... name = StringCol() ... >>> class Address(SQLObject): ... address = StringCol() ... person = ForeignKey('Person') ... >>> Person.createTable() [] >>> Address.createTable() |
上面的程式碼建立了2個簡單的表:person 和 address 。為了建立和插入記錄到這2個表,我們簡單例項化一個person 例項和 一個 address 例項:
1 2 3 4 5 6 7 |
>>> p = Person(name='person') >>> a = Address(address='address', person=p) >>> p >>> a <address> |
為了獲得或檢索新記錄, 我們用神奇的 q 物件關聯到 Person 和 Address 類:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
>>> persons = Person.select(Person.q.name == 'person') >>> persons >>> list(persons) [] >>> p1 = persons[0] >>> p1 == p True >>> addresses = Address.select(Address.q.person == p1) >>> addresses >>> list(addresses) [ <address>] >>> a1 = addresses[0] >>> a1 == a True |
Storm
Storm 是一個介於 單個或多個資料庫與Python之間 對映物件的 Python ORM 。為了支援動態儲存和取回物件資訊,它允許開發者構建跨資料表的複雜查詢。它由Ubuntu背後的公司 Canonical公司用Python開發的,用在 Launchpad 和 Landscape 應用中,後來在2007年作為自由軟體釋出。這個專案在LGPL許可下發布,程式碼貢獻者必須受讓版權給Canonical公司。
像 SQLAlchemy 和 SQLObject 那樣, Storm 也對映表到類,行到例項和欄位到屬性。相對另外2個庫, Stom中 table class 不需要是框架特定基類 的子類 。在 SQLAlchemy中,每個 table class 是 sqlalchemy.ext.declarative.declarative_bas 的一個子類。 而在SQLOjbect中,每個table class是 的 sqlobject.SQLObject 的子類。
類似於 SQLAlchemy, Storm 的 Store 物件對於後端資料庫就像一個代理人, 所有的操作快取在記憶體,一當提交方法在store上被呼叫就提交到資料庫。每個 store 持有自己的Python資料庫物件對映集合,就像一個 SQLAlchemy session 持有不同的 Python物件集合。
指定版本的 Storm 可以從 下載頁面 下載。在這篇文章裡,示例程式碼是使用 0.20 版本的Storm寫的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
>>> from storm.locals import Int, Reference, Unicode, create_database, Store >>> >>> >>> db = create_database('sqlite:') >>> store = Store(db) >>> >>> >>> class Person(object): ... __storm_table__ = 'person' ... id = Int(primary=True) ... name = Unicode() ... >>> >>> class Address(object): ... __storm_table__ = 'address' ... id = Int(primary=True) ... address = Unicode() ... person_id = Int() ... person = Reference(person_id, Person.id) ... |
上面的程式碼建立了一個 sqlite 記憶體資料庫,然後用 store 來引用該資料庫物件。一個Storm store 類似 SQLAlchemy的 DBSession物件,都管理 附屬於其的例項物件 的生命週期。例如,下面的程式碼建立了一個 person 和 一個 address, 然後通過重新整理 store 都插入記錄。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
>>> store.execute("CREATE TABLE person " ... "(id INTEGER PRIMARY KEY, name VARCHAR)") >>> store.execute("CREATE TABLE address " ... "(id INTEGER PRIMARY KEY, address VARCHAR, person_id INTEGER, " ... " FOREIGN KEY(person_id) REFERENCES person(id))") >>> person = Person() >>> person.name = u'person' >>> print person >>> print "%r, %r" % (person.id, person.name) None, u'person' # Notice that person.id is None since the Person instance is not attached to a valid database store yet. >>> store.add(person) >>> print "%r, %r" % (person.id, person.name) None, u'person' # Since the store hasn't flushed the Person instance into the sqlite database yet, person.id is still None. >>> store.flush() >>> print "%r, %r" % (person.id, person.name) 1, u'person' # Now the store has flushed the Person instance, we got an id value for person. >>> address = Address() >>> address.person = person >>> address.address = 'address' >>> print "%r, %r, %r" % (address.id, address.person, address.address) None, , 'address' >>> address.person == person True >>> store.add(address) >>> store.flush() >>> print "%r, %r, %r" % (address.id, address.person, address.address) 1, , 'address' |
為了獲得或檢索已插的 Person 和 Address 物件, 我們呼叫 store.find() 來查詢:
1 2 3 4 5 6 7 8 |
>>> person = store.find(Person, Person.name == u'person').one() >>> print "%r, %r" % (person.id, person.name) 1, u'person' >>> store.find(Address, Address.person == person).one() >>> address = store.find(Address, Address.person == person).one() >>> print "%r, %r" % (address.id, address.address) 1, u'address' |
Django 的 ORM
Django 是一個免費開源的緊嵌ORM到其系統的web應用框架。在它首次釋出後,得益於其易用為Web而備的特點,Django越來越流行。它在2005年七月在BSD許可下發布。因為Django的ORM 是緊嵌到web框架的,所以就算可以也不推薦,在一個獨立的非Django的Python專案中使用它的ORM。
Django,一個最流行的Python web框架, 有它獨有的 ORM。 相比 SQLAlchemy, Django 的 ORM 更吻合於直接操作SQL物件,操作暴露了簡單直接對映資料表和Python類的SQL物件 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
$ django-admin.py startproject demo $ cd demo $ python manage.py syncdb Creating tables ... Creating table django_admin_log Creating table auth_permission Creating table auth_group_permissions Creating table auth_group Creating table auth_user_groups Creating table auth_user_user_permissions Creating table auth_user Creating table django_content_type Creating table django_session You just installed Django's auth system, which means you don't have any superusers defined. Would you like to create one now? (yes/no): no Installing custom SQL ... Installing indexes ... Installed 0 object(s) from 0 fixture(s) $ python manage.py shell |
因為我們在沒有先建立一個專案時不能夠執行Django程式碼,所以我們在前面的shell建立一個Django demo 專案,然後進入Django shell來測試我們寫的 ORM 例子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
# demo/models.py >>> from django.db import models >>> >>> >>> class Person(models.Model): ... name = models.TextField() ... class Meta: ... app_label = 'demo' ... >>> >>> class Address(models.Model): ... address = models.TextField() ... person = models.ForeignKey(Person) ... class Meta: ... app_label = 'demo' ... |
上面的程式碼宣告瞭2個Python 類,Person 和 Address,每一個都對映到資料庫表。在執行任意資料庫操作程式碼之前,我們需要先在本地的sqlite資料庫建立表。
1 2 3 4 5 6 7 |
python manage.py syncdb Creating tables ... Creating table demo_person Creating table demo_address Installing custom SQL ... Installing indexes ... Installed 0 object(s) from 0 fixture(s) |
為了插入一個 person 和一個 address 到資料庫,我們例項化相應物件並呼叫這些物件的save() 方法。
1 2 3 4 5 6 7 8 9 |
>>> from demo.models import Person, Address >>> p = Person(name='person') >>> p.save() >>> print "%r, %r" % (p.id, p.name) 1, 'person' >>> a = Address(person=p, address='address') >>> a.save() >>> print "%r, %r" % (a.id, a.address) 1, 'address' |
為了獲得或檢索 person 和 address 物件, 我們用model類神奇的物件屬性從資料庫取得物件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
>>> persons = Person.objects.filter(name='person') >>> persons [] >>> p = persons[0] >>> print "%r, %r" % (p.id, p.name) 1, u'person' >>> addresses = Address.objects.filter(person=p) >>> addresses [ <address>] >>> a = addresses[0] >>> print "%r, %r" % (a.id, a.address) 1, u'address' |
peewee
peewee 是一個小的,表示式的 ORM。相比其他的 ORM,peewee 主要專注於極簡主義,其API簡單,並且其庫容易使用和理解。
1 2 3 4 5 6 7 8 9 10 11 12 |
pip install peewee Downloading/unpacking peewee Downloading peewee-2.1.7.tar.gz (1.1MB): 1.1MB downloaded Running setup.py egg_info for package peewee Installing collected packages: peewee Running setup.py install for peewee changing mode of build/scripts-2.7/pwiz.py from 644 to 755 changing mode of /Users/xiaonuogantan/python2-workspace/bin/pwiz.py to 755 Successfully installed peewee Cleaning up... |
為了建立資料庫模型對映,我們實現了一個Person 類 和一個Address類 來對映對應的資料庫表。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
>>> from peewee import SqliteDatabase, CharField, ForeignKeyField, Model >>> >>> db = SqliteDatabase(':memory:') >>> >>> class Person(Model): ... name = CharField() ... class Meta: ... database = db ... >>> >>> class Address(Model): ... address = CharField() ... person = ForeignKeyField(Person) ... class Meta: ... database = db ... >>> Person.create_table() >>> Address.create_table() |
為了插入物件到資料庫,我們例項化物件並呼叫了它們的save() 方法。從檢視的物件建立這點來看,peewee類似於Django。
1 2 3 4 |
>>> p = Person(name='person') >>> p.save() >>> a = Address(address='address', person=p) >>> a.save() |
為了從資料庫獲得或檢索物件, 我們select 了類各自的物件。
1 2 3 4 5 6 7 8 |
>>> person = Person.select().where(Person.name == 'person').get() >>> person >>> print '%r, %r' % (person.id, person.name) 1, u'person' >>> address = Address.select().where(Address.person == person).get() >>> print '%r, %r' % (address.id, address.address) 1, u'address' |
SQLAlchemy
SQLAlchemy 是Python程式語言裡,一個在MIT許可下發布的開源工具和SQL ORM。它首次釋出於2006年二月,由Michael Bayer寫的。它提供了 “一個知名企業級的持久化模式的,專為高效率和高效能的資料庫訪問設計的,改編成一個簡單的Python域語言的完整套件”。它採用了資料對映模式(像Java中的Hibernate)而不是Active Record模式(像Ruby on Rails的ORM)。
SQLAlchemy 的工作單元 主要使得 有必要限制所有的資料庫操作程式碼到一個特定的資料庫session,在該session中控制每個物件的生命週期 。類似於其他的ORM,我們開始於定義declarative_base()的子類,以對映表到Python類。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
>>> from sqlalchemy import Column, String, Integer, ForeignKey >>> from sqlalchemy.orm import relationship >>> from sqlalchemy.ext.declarative import declarative_base >>> >>> >>> Base = declarative_base() >>> >>> >>> class Person(Base): ... __tablename__ = 'person' ... id = Column(Integer, primary_key=True) ... name = Column(String) ... >>> >>> class Address(Base): ... __tablename__ = 'address' ... id = Column(Integer, primary_key=True) ... address = Column(String) ... person_id = Column(Integer, ForeignKey(Person.id)) ... person = relationship(Person) ... |
在我們寫任何資料庫程式碼前,我們需要為資料庫session建立一個資料庫引擎。
1 2 |
>>> from sqlalchemy import create_engine >>> engine = create_engine('sqlite:///') |
一當我們建立了資料庫引擎,可以繼續建立一個資料庫會話,併為所有之前定義的 Person和Address 類建立資料庫表。
1 2 3 4 |
>>> from sqlalchemy.orm import sessionmaker >>> session = sessionmaker() >>> session.configure(bind=engine) >>> Base.metadata.create_all(engine) |
現在,session 物件物件變成了我們工作單元的建構函式,將和所有後續資料庫操作程式碼和物件關聯到一個通過呼叫它的 __init__() 方法構建的資料庫session上。
1 2 3 4 5 |
>>> s = session() >>> p = Person(name='person') >>> s.add(p) >>> a = Address(address='address', person=p) >>> s.add(a) |
為了獲得或檢索資料庫中的物件,我們在資料庫session物件上呼叫 query() 和 filter() 方法。
1 2 3 4 5 6 7 8 |
>>> p = s.query(Person).filter(Person.name == 'person').one() >>> p >>> print "%r, %r" % (p.id, p.name) 1, 'person' >>> a = s.query(Address).filter(Address.person == p).one() >>> print "%r, %r" 1, 'address' |
請留意到目前為止,我們還沒有提交任何對資料庫的更改,所以新的person和address物件實際上還沒儲存在資料庫中。 呼叫 s.commit() 將會提交更改,比如,插入一個新的person和一個新的address到資料庫中。
1 2 |
>>> s.commit() >>> s.close() |
Python ORM 之間對比
對於在文章裡提到的每一種 Python ORM ,我們來列一下他們的優缺點:
SQLObject
優點:
- 採用了易懂的ActiveRecord 模式
- 一個相對較小的程式碼庫
缺點:
- 方法和類的命名遵循了Java 的小駝峰風格
- 不支援資料庫session隔離工作單元
Storm
優點:
- 清爽輕量的API,短學習曲線和長期可維護性
- 不需要特殊的類建構函式,也沒有必要的基類
缺點:
- 迫使程式設計師手工寫表格建立的DDL語句,而不是從模型類自動派生
- Storm的貢獻者必須把他們的貢獻的版權給Canonical公司
Django’s ORM
優點:
- 易用,學習曲線短
- 和Django緊密集合,用Django時使用約定俗成的方法去運算元據庫
缺點:
- 不好處理複雜的查詢,強制開發者回到原生SQL
- 緊密和Django整合,使得在Django環境外很難使用
peewee
優點:
- Django式的API,使其易用
- 輕量實現,很容易和任意web框架整合
缺點:
- 不支援自動化 schema 遷移
- 多對多查詢寫起來不直觀
SQLAlchemy
優點:
- 企業級 API,使得程式碼有健壯性和適應性
- 靈活的設計,使得能輕鬆寫複雜查詢
缺點:
- 工作單元概念不常見
- 重量級 API,導致長學習曲線
總結和提示
相比其他的ORM, SQLAlchemy 意味著,無論你何時寫SQLAlchemy程式碼, 都專注於工作單元的前沿概念 。DB Session 的概念可能最初很難理解和正確使用,但是後來你會欣賞這額外的複雜性,這讓意外的時序提交相關的資料庫bug減少到0。在SQLAlchemy中處理多資料庫是棘手的, 因為每個DB session 都限定了一個資料庫連線。但是,這種型別的限制實際上是好事, 因為這樣強制你絞盡腦汁去想在多個資料庫之間的互動, 從而使得資料庫互動程式碼很容易除錯。
在未來的文章中,我們將會完整地披露更高階的SQLAlchemy用例, 真正領會無限強大的API。