【Python】 SQLAlchemy的初步使用


SQLAlchemy

  在很多Python的web框架中都整合進了SQLAlchemy這個主要發揮ORM作用的模塊。所謂ORM,就是把復雜的SQL語句給包裝成更加面向對象,易於理解的樣子。在操作數據庫的時候,我們可以用比較底層的MySQLdb之類的模塊來直接連接執行SQL語句,但是在實際開發過程中,開發人員一次次寫SQL也是很煩的,ORM就是一個解決之道。

  SQLAlchemy是一個獨立的模塊,不過被很多框架都囊括其中。比如有flask-SQLAlchemy這樣的flask擴展。但是與其學習一些有限融合到web框架中的“變異版”SQLAlchemy倒不如學習原生的。好吧其實我言重了。。flask-SQLAlchemy和原生的也沒多大差別,總之體會一下通過ORM操作數據庫的快感是目的。

 

■  基本配置和使用

  在pip install sqlalchemy安裝模塊完成之后,首先要確認數據庫的情況。這里默認用MySQL數據庫,並且在DB系統中已經建立了名為"flask_DB"的數據庫,庫中目前沒有表。

  另外,為了順利連接數據庫,還需要一個支持python的數據庫驅動程序。這里用MySQL的話之前已經裝過MySQLdb了,似乎這就可以了。實際上我是今天凌晨2點多搞的這個東西,當時已經快要成仙了。。腦子一團糊,總之不知怎么七搞八搞就可以了。。

  ●  連接數據庫

  外部連接數據庫時,表明數據庫身份的一般都是一個URL。在SQLAlchemy中可以把這個URL包裝到一個所謂的引擎里面,利用這個引擎可以擴展出很多ORM中有用的對象。比如下面這樣:

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

engine = create_engine("mysql+mysqldb://weiyz:123456@192.168.1.101:3306/flask_DB")

Session = sessionmaker(bind=engine)

 

 

  可以看到,這個URL要具體到哪個數據庫,因為在SQLAlchemy操作時最高單位是以數據庫為准的,不能跨庫操作數據。Session是一個會話或者說事務類,通過把engine傳入sessionmaker方法獲得,但是它不是一個真的會話對象了,要通過調用它來得到會話對象,下面會提到具體用法。

  這樣子就算我們配置好了和數據庫的連接了,但是還沒有正式啟用所以庫,數據庫服務乃至於這台機器本身存不存在這些都還沒有經過校驗

  ●  表的表示

  因為SQLAlchemy一次只讓操作一個庫,所以基本上操作的主要對象就是表了。表這個東西也是有意思,如果你想把這個表抽象成一個ORM對象,你覺得應該怎么做?其實表本身就像是一個class,它具有一些特征的屬性(各個字段),把這個class不斷地實例化后得到的就會各種實例(每一行的數據)。事實上SQLAlchemy就是這么把表抽象成一個類的。比如下面這樣:

from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import  String,Column,Integer

engine = #引用上面的engine
Session = sessionmaker(bind=engine)

Base = declarative_base()

class Student(Base):    #必須繼承declaraive_base得到的那個基類
    __tablename__ = "Students"    #必須要有__tablename__來指出這個類對應什么表,這個表可以暫時在庫中不存在,SQLAlchemy會幫我們創建這個表
    Sno = Column(String(10),primary_key=True)    #Column類創建一個字段
    Sname = Column(String(20),nullable=False,unique=True,index=True)    #nullable就是決定是否not null,unique就是決定是否unique。。這里假定沒人重名,設置index可以讓系統自動根據這個字段為基礎建立索引
    Ssex = Column(String(2),nullable=False)
    Sage = Column(Integer,nullable=False)
    Sdept = Column(String(20))

    def __repr__(self):
        return "<Student>{}:{}".format(self.Sname,self.Sno)

  Student類中的__repr__方法不是必須的,但是可以寫在這里來使得調試時更加容易分辨清楚誰是誰。。一些注意事項我都寫在注釋里了。

  有了這么一個類就相當於有了一張表,可以編寫多個類來得到多張表,需要注意它們必須都繼承上面的Base類,否則它們將會是一個孤立於SQLAlchemy的一個類,不會和數據庫發生實質聯系。

  創建表類的時候可以添加一個id字段,這個字段必須是id = Column(Integer, primary_key=True),然后每次初始化的時候就不用管它,SQLAlchemy會為我們自動維護這個id字段,給每個初始化出來的數據對象都賦予一個獨特的id。據觀察id是從1開始逐漸網上+1的,而且這個id是不會回頭的,也就是說即使把前面10條記錄都刪掉了,只要表還在,新增記錄的id是從11開始的。

  ●  啟動實際連接

  我們配置好了數據庫的連接和會話類,同時也抽象好了表的情況,接下來就只剩下正式地開始連接操作了。連接操作一般可以這么做:

###之前上面兩段代碼中出現過的東西就不再聲明了###
Base.metadata.create_all(engine)    #這就是為什么表類一定要繼承Base,因為Base會通過一些方法來通過引擎初始化數據庫結構。不繼承Base自然就沒有辦法和數據庫發生聯系了。

session = Session()    #實例化了一個會話(或叫事務),之后的所有操作都是基於這個對象的

#既然是事務對象,session必然有以下這些方法
session.commit()    #提交會話(事務)
session.rollback()    #回滾會話
session.close()    #關閉會話

   ●  關於數據庫中數據的對象在session中的四種狀態

  ORM模型很方便地將數據庫中的一條條記錄轉變成了python中的一個個對象,有時候我們會想當然地把兩者完全等同起來,但是不要忘了,兩者之間還必須有session這個中間的橋梁。因為有session在中間做控制,所以必須注目對象和記錄之間一個狀態上的差別。一般而言,一個數據的對象可以有四種不同的和session關聯的狀態。從代碼的流程上看:

session = Session()    #創建session對象
frank = Person(name='Frank')    #數據對象得到創建,此時為Transient狀態
session.add(frank)    #數據對象被關聯到session上,此時為Pending狀態
session.commit()    #數據對象被推到數據庫中,此時為Persistent狀態
session.close()    #關閉session對象
print frank.name    #此時會報錯DetachedInstanceError,因為此時是Detached狀態。

new_session = Session()
print new_session.query(Person).get(1).name    #可以查詢到數據
new_session.close()

 

  四個對象的狀態分別是上面四種,Transient/Pending/Persistent/Detached。其中需要比較注意的是Detached狀態,這就是在說,並不是我在python中創建了一個數據記錄的對象我就可以沒有限制地訪問它,可以看到訪問繼承自Base類的這么一個對象時必須要使其身處一個session的上下文中,否則是報錯的!

 

■  通過SQLAlchemy操作數據庫

  首先要說的是下面的內容默認都是通過ORM來進行操作,如果直接執行SQL的話,那么可以調用session.execute("SQL").fetchone/all(),這個和MySQLdb是一個意思,返回的也都是元組(的列表)這樣子的數據。

  操作數據庫無非是增刪查改,而正如上面所說的,通過SQLAlchemy的ORM方式來操作數據庫的話都是基於session這個會話對象的。下面就來簡單說說如何進行增刪查改

  這里想額外提一下的是,基於SQLAlchemy和ORM的操作不能獨立於表類定義的。從下面的實例中可以看到,比如session做一個查詢操作,后面要跟query(類名)。一般來說,最好能保證類中屬性和數據庫中表結構的完全對應,這是墜吼的。如果實在不能保證,比如有個現成的表在數據庫中然而我只能知道部分表的信息,所以我只能構造出一個“閹割版”的類,這樣的話是什么情況呢?經試驗,編碼層面的校驗是以類為准,比如某個類中沒寫某個屬性,那么即便這個屬性確實存在於表中也無法訪問到它,會報錯“AttributeError”,而訪問那些類和表中都存在的屬性是可行的。這是針對類比表少了東西的情況,如果類比表多了一些屬性,那么直接報錯,不會給ORM機會訪問庫里的數據。從要構造類來模擬表結構這個角度看,ORM似乎還不如MySQLdb。。不過不要忘了,ORM的優勢就是在於把數據轉化為語言中的對象。所以構造一個類來模擬表結構的工作也無可厚非。。

  ●  新增

  向一個表插入數據的話相當於是需要插入若干個這個表類的實例。所以可以初始化一些表類的實例后把它們作為數據源,通過session.add或者add_all方法來插入新數據。

###之前代碼中出現的變量這里不再聲明了###
student = Student(Sno='10001',Sname='Frnak',Ssex='M',Sage=22,Sdept='SFS')

session.add(student)
session.commit()    #不要忘了commit
session.close()

#可以通過一些手段實例化一批student出來放到一個列表students中,然后調用add_all
session.add_all(students)
session.commit()
session.close()

  ●  查詢

session.query(Student).filter(Student.Sname == 'Frank').first()

  這是一條比較經典的ORM查詢語句,需要注意的地方很多。首先用了query方法來查詢,方法參數指明了查詢哪張表,這個參數應該是某個類,而跟類中的__tablename__的值是沒多大關系的。然后調用了filter方法,filter方法的參數有些特別,是一個判斷表達式(注意是兩個等號!),它表示查詢的條件。注意左邊的字段名一定要加上類名.字段名,如果只寫字段名SQLAlchemy將無法識別這個字段。最后是first()方法來表明返回第一個查詢到的結果,也可以調用all()來返回所有結果的列表。如果沒有這個first或者all的話那么返回的東西如果打印出來是一個SQL語句,對應着這個ORM查詢。當查詢結果不符合預期時可以打印出這個SQL來看一看問題在哪里。語句最終返回的一行數據的對象(或者它們組成的列表,first和all的區別),每個這種對象都可以通過object.attribute的方式來訪問每個字段的值。

  在filter這個位置,還可以添加多種多樣的過濾器來實現靈活的查詢,比如

  filter(Student.Sname != 'Frank')

  filter(Student.id >= 10)

  除了filter方法外還可以用filter_by方法,filter_by和filter的區別在於filter_by里面寫得是kwargs形式的參數,且參數不用帶表名。比如上面的filter(Student.Sname=='Frank')可以改寫成filter_by(Sname="Frank")。個人感覺filter_by更加符合常規思維一點。不過它只能用於等值查詢,要不等值,大於,小於查詢的時候還是得用filter。

  另外還有like方法進行模糊查詢。用法是filter(Student.Sname.like("F%")),這個是查找所有F開頭名字的學生的記錄。

  還有in_方法來進行“包括”過濾,用法filter(Student.Sname.in_(['Frank','Takanashi']))。如果在filter方法所有參數最前面加上一個~,就是“非包括”過濾,比如剛才那個加上的話就是查詢所有不叫Frank和Takanashi的學生了。

  有is_方法,主要用來進行判空操作。filter(Student.Sdept.is_(None))就選擇出了所有未填寫院系信息的學生的記錄,如果是filter(Student.Sdept.isnot(None))就是選擇非空記錄。其實is_和isnot可以通過filter中的==和!=來實現,兩種方法效果是一樣的。

  ~在filter參數中表示非邏輯,上面講過了

  還有and_和or_兩個方法來表示並或邏輯,兩者不是filter返回對象自帶的方法,需要額外從sqlalchemy導入:

from sqlalchemy import and_,or_
print session.query(Student).filter(and_(Student.Sdept == 'SFS' , Student.Sage < 22)).all()
#上句選出了所有外院但是年齡小於22的學生記錄,經測試也可以寫3個及以上的條件參數
#or_方法也是類似的,不再多說

  上面說的所有代替filter的方法頂多是代替了filter,后面還是要加上first或者all來獲取具體結果。還有一個不需要額外加first和all的方法,就是在query的返回后面直接加get("主鍵的值")來獲取特定主鍵的那一行數據的對象。

  以上所有查詢中query中只有一個參數(而且基本上都是表名),這其實相當於是SELECT 表名.* FROM 表名 [WHERE xxx]。其實在query方法參數中可以寫子級字段,也可以寫多個參數。比如:

session.query(Student.id,Student.name).filter(Student.name.like("F%")).all()    #查詢了所有名字F開頭的學生的學號和姓名

 

   有意思的是,如果query參數寫的只是一個表名,那么返回的列表是對象的列表。而如果我寫出來若干個字段名的話,返回的列表其中每一項都是一個tuple,一個tuple里面就是一行內指定字段的值了。

  ●  刪除

  刪除相對簡單一些:

target = session.query(Student).get("10001")
session.delete(target)
session.commit()

  ●  修改

  修改的話直接在對象身上修改,修改可以反映到數據庫中去

target = session.query(Student).filter(Student.Sname == "Kim").first()
target.Sname = "Kimmy"
session.commit()
session.close()

##之后在到數據庫后台,或者這里查詢看到的就是Kimmy了
print session.query(Student).filter_by(Sname="Kimmy").first()    #有結果

 

■  表之間的關系

  作為關系型數據庫,關系可以說是最重要的一種邏輯結構了。上述所有涉及到表的設計中,都沒有涉及到表之間的關系的確認。下面簡單講一下表之間的關系如何表現出來。

  從編碼層面上來講,要進行兩個表的關聯主要做兩件事,1.明確外鍵。2.用relationship函數關聯兩者。只聲明了外鍵是什么沒有relationship函數返回的屬性做橋梁的話,關系在操作上的便利性體現不出來;如果只聲明了關系而不明確外鍵的話,SQLAlchemy會不知道兩者通過何種字段關聯,從而報錯。

  下面來考慮這樣一個場景:有一張User表,上面有id和name兩個字段,一個id當然只有一個名字。另一張表Address記錄了郵箱地址,有id,addr和user_id三個字段,因為一個user可能有多個郵箱地址,所以沒有直接用user_id作為主鍵,而另起了一個主鍵。現在我想通過一個名字,查詢到所有這個人的郵箱地址可以怎么做?一般而言,我可能會先查詢User表中的id,然后去Address表的user_id字段中找相同的id。這樣操作比較繁瑣。如果可以為兩張表添加一個關系,聯系兩張表在一起,這樣從編碼的角度來說就可以一句話就解決問題,比如:

from sqlalchemy import create_engine,Column,String,Integer,ForeignKey
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker,relationship

engine = create_engine("mysql+mysqldb://weiyz:123@localhost:3306/test")
Session = sessionmaker(bind=engine)
Base = declarative_base()

class User(Base):
    __tablename__ = 'users'
    id = Column(Integer,primary_key=True)
    name = Column(String(20),nullable=False)

    addresses = relationship('Address')
class Address(Base):
    __tablename__ = 'address'
    id = Column(Integer,primary_key=True)
    address = Column(String(20),nullable=False)
    user_id = Column(Integer,ForeignKey('users.id'))  #請注意,設置外鍵的時候用的是表名.字段名。其實在表和表類的抉擇中,只要參數是字符串,往往是表名;如果是對象則是表類對象。

    user = relationship('User')

  經過上面的對表的定義,數據庫中的users和address兩張表就通過外鍵有了聯系,為了利用好這種聯系,我們就可以靈活運用User類中的addresses屬性和Address類中的user屬性了。這里說的運用,既可以指在類內直接調用,比如在Address中可以加入一個__repr__(self)方法,返回的信息中當然最好有地址所屬人的名字,在沒有聯系時很難做到這一點,但是有了聯系之后,現在只需要self.user.name就可以了。

  再比如說,回到這節最前面說到的那個場景。想通過一個名字直接搜到他的所有郵箱地址,那么就可以直接調用屬性:

  session.query(User).filter_by(name="xxx").first().addresses

  神奇的一點是,SQLAlchemy會根據關系的對應情況自動給關系相關屬性的類型。比如這里的User下面的addresses自動是一個list類型,而Address下面的user由於設定了外鍵的緣故,一個地址最多只能應對一個用戶,所以自動識別成一個非列表類型。

  ●  用backref做一點小改進

  上面的例子中,我們分別為User和Address兩個類分別添加了連到對方的關系。這么寫好像略顯繁瑣一點,用from sqlalchemy.orm import backref的這個backref可以更加方便地一次性寫清雙向的關系。這就是直接把backref='user'作為參數添加在addresses = relationship('Adress',backref='user')中,然后把user=relationship(xxx)直接刪掉。或者反過來把backref添加在user中再把addresses刪掉。

  ●  關於relationship方法的一些參數

  relationship方法除了傳遞一個類名和可以加上backref之外,還有一些其他的參數。比如還有uselist=True/False這樣一個參數。讓uselist成為False之后,所有原先因為有多條結果而成為list類型的關系屬性,比如上面例子中的User類下的addresses這個屬性,會變成一個單個的變量。同時對於確實是有多個結果的情況系統會給出一個警告。總之,讓uselist=False之后可以讓這個關系每次通過關系來調用數據的時候都是fetchone而不是fetchall。

  relationship還有另外一個參數,就是lazy。這個參數更加復雜一些。一般而言,就像上面的例子一樣,我們通過關系屬性來通過一個表的查詢對象查詢另一個表中的數據,形式上來說是直接.addresses就得到結果了。這是lazy="select"(默認是select)的結果。有時候,直接這么獲得的數據比較多比較大,比如一個人有成千上萬個郵箱地址的話,或許我不想這么直白地列出所有數據,而是對這些數據做一個二次加工。這時候就要用到lazy="dynamic"模式。在這個模式下,.addresses得到的仍然是一個query對象,所以我們可以進一步調用filter之類的方法來縮小范圍。為了提高lazy的使用正確性,SQLAlchemy還規定了,不能再多對一,一對一以及uselist=False的這些模式的關系中使用。

  ●  關於多對多關系

  上面這個例子一個用戶對應多個地址,而一個地址只對應一個用戶,所以說是一個一對多關系。在現實問題中還有很多多對多關系,比如老師和班級,一個老師可能在很多班級任教,而一個班級顯然也有很多不同科目的老師。這種就是多對多關系。

  在利用ORM進行多對多關系的抽象時通常需要一個第三表來承載這種關系,且在老師和班級表中都不能設置外鍵,一旦設置外鍵就表明這個每一行這個字段的值只能對應一個值了,又變回一對多關系。總之多對多關系可以像下例中一樣構造:

class Class(Base):
    __tablename__ = 'class'
    class_id = Column(Integer,primary_key=True)
    name = Column(String(20),nullable=False)
    #這里不加teacher = Column(Integer,ForeignKey('teacher.teacher_id'))之類的字段,因為這樣又會變回一對多關系
    #為了篇幅其他字段就不加了,意思意思
    class_teacher = relationship('ClassTeacher',backref='class')

class Teacher(Base):
    __tablename__ = 'teacher'
    teacher_id = Column(Integer,primary_key=True)
    name = Column(String(20),nullable=False)
    #同樣,這里也不用加class = xxx
    teacher_class = relationship('ClassTeacher',backref='teacher')

class ClassTeacher(Base):
    __tablename__ = 'class_teacher'    #這就是所謂的一張視圖表?沒有實際存在數據,但是憑借關系型數據庫的特點可以體現出一些數據關系
    teacher_id = Column(Interger,ForeignKey('teacher.teacher_id'),primary_key=True)
    class_id = Column(Interger,ForeignKey('class.class_id'),primary_key=True)
    #這張第三表中有兩個主鍵,表示不能有class_id和teacher_id都相同的兩項

###可以看到,通過第三表做橋梁,把多對多關系架構了起來。實際運用可以參考下面###
class = session.query(Class).filter(Class.name == '三年二班').first() for class_teacher_rel in class.class_teacher: print class_teacher_rel.teacher.name
"""這里比較繞了,首先class是查詢得到的Class表中的一條記錄的對象,調用class_teacher屬性,通過在Class中設置的關系鏈接到ClassTeacher表中,得到一個列表
,每項都是這個class相關的teacher的關系(不是teacher對象本身而是ClassTeacher對象,可以理解成是一種關系的對象)。
然后根據在Teacher中定義的backref,通過ClassTeacher反向調用teacher就可以獲得Teacher對象了。接下來再調用name屬性就沒什么可說的了。"""

 

 

■  一個比較好的實踐

  下面這個是摘錄自書上的一個簡單的ORM程序,雖然它只針對一個表進行數據的操作,但是利用了上下文管理器和包裝操作方法來讓需要進行數據庫操作的人幾乎感覺不到SQL的影子,完全就是很pythonic的方法來執行操作的。下面把這個實踐整個寫下來,順便記錄一點小技巧。

  首先編寫orm.py文件。在這個文件中定義了基本的數據庫連接和配置信息,創建了數據庫連接引擎,另外還通過繼承Base類來定義了表結構。

 1 #####orm.py#####
 2 # coding=utf-8
 3
 4 from sqlalchemy.ext.declarative import declarative_base
 5 from sqlalchemy import create_engine,Column,Integer,String
 6
 7 db_connect_string = "mysql+mysqldb://weiyz:123456@localhost:3306/test"
 8 engine = create_engine(db_connect_string)
 9
10 Base = declarative_base()
11
12 class Account(Base):
13     __tablename__ = 'account'
14
15     id = Column(Integer,primary_key=True)
16     user_name = Column(String(50),nullable=False)
17     password = Column(String(200),nullable=False)
18     salary = Column(Integer)
19
20     def is_active(self):    #這個和下面幾個方法都是意思意思,就是說表類中其實也可以定義一些方法,讓數據對象的操作可以更優雅好看
21         return True
22
23     def is_anonymous(self):
24         return False
25
26     def get_id(self):
27         return self.id
28
29     def get_authorized(self):
30         return True
31
32     def __repr__(self):
33         return "<type 'Account'>{}:{}".format(self.id,self.user_name)
34
35 Base.metadata.create_all(engine)    #原文中少了這句,這樣的話假如原來庫中沒有定義的表的話就會報錯

 

  然后是編寫了orm_ope.py,這里主要分成兩部分,上半部分構造了對話對象的“生成器”,並且用了contextlib這個上下文管理工具包裝進了對話對象的commit,rollback,close等方法,讓真正的操作方法不用管這些操作。下半部分則是對一些操作的封裝,把原來復雜的session.query()之類的語句凝練成一個函數,調用起來更加方便,看不出任何數據庫的痕跡。

 1 #####orm_ope.py#####
 2 # coding=utf-8
 3
 4 from sqlalchemy.orm import scoped_session,sessionmaker
 5 import orm  #把上面orm中定義好的東西拿來用
 6
 7 SessionType = scoped_session(sessionmaker(bind=orm.engine))
 8 #這里引入了scoped_session,相比於直接的sessionmaker,scoped_session返回的對象構造出來的對話對象是全局唯一的,不同的對話對象實例都指向一個對象引用    。
 9 def GetSession():
10     return SessionType()
11
12 from contextlib import contextmanager
13 ###contextlib的用法可以看相關的那篇,這里不多說了###
14 @contextmanager
15 def session_scope():
16     session = GetSession()
17     try:
18         yield session
19         session.commit()
20
21     except Exception as e:
22         session.rollback()
23         raise
24     finally:
25         session.close()
26
27 from sqlalchemy import or_
28 def GetAccount(id=None,user_name=None):
29     with session_scope() as session:
30         res = session.query(orm.Account).filter(or_(orm.Account.id == id,orm.Account.user_name == user_name)).first()
31         yield res
32         #獲取信息時,由於上下文管理的原因,如果寫return,那么總是在返回值返回上層調用之前session就已經close了。這導致的問題就是報錯DetachedInstanceError。所以這里一定要寫yield
33
34 def GetAllAccount():
35     with session_scope() as session:
36         for account in session.query(orm.Account).all():
37             yield account
38
39 def InsertAccount(user,password,salary):
40     with session_scope() as session:
41         account = orm.Account(user_name=user,password=password,salary=salary)
42         session.add(account)
43
44 def DeleteAccount(id):
45     with session_scope() as session:
46         account = session.query(orm.Account).get(id)
47         session.delete(account)

  可以包裝的方法還有很多,比如UpdateAccount之類的,這個不復雜就不多寫了。在定義完這些內容之后,就可以直接調用這些已經包裝好的方法來進行數據庫操作:

###main.py###
# coding=utf-8

from orm_ope import GetAccount,GetAllAccount,InsertAccount,DeleteAccount,ClearAccount
from faker import Factory
import random
import hashlib

faker = Factory.create()

if __name__ == '__main__':
    # for acc in GetAllAccount():
    #     print acc
    ClearAccount()
    for i in range(10):
        user = faker.name()
        md5 = hashlib.md5()
        md5.update(faker.word())
        password = md5.hexdigest()
        InsertAccount(user=user,password=password,salary=round(random.random()*10000,2))

 

  可能有人會覺得這樣包裝方法雖然是方便了,但是帶來問題也不小,比如這里所有方法都只針對Account一張表,如果我有很多很多表需要操作,那么我是不是要寫這么多方法呢?其實這里說的只是一種使代碼更加好看的一種方法,今天也在網上看到有人讓query_user = session.query(User),query_account = session.query(Account)這樣簡單的優化,可以用不同的query對象來靈活應用。總之具體問題具體分析吧,可能不存在一種最好的包裝方法,在優雅和工作量上做一個權衡就好。

  *今天突然想到,其實不用把每個表的增刪查改單獨做成一個函數,如果把表本身也作為一個參數傳遞給函數的話,所有表的增刪查改都可以統一到四個函數里面了。比如下面這樣一個函數:

def Insert(table=None,**kwargs):
  if not table:
    raise Exception('table is not clarified!')
  with session_scope() as session:
    obj = table(**kwargs)
    session.add(obj)

   刪查改的也都是大同小異,可以自己再想想。刪查改可能要比增更加復雜,因為涉及到了傳遞進來的參數要進行處理的問題,這時不要忘了kwargs是一個字典的形式,或者不要kwargs,對方法的參數做一些規定來簡化我們的這個函數。

 

■  利用ORM查詢時有用的其他一些方法

  session.query(xxx)得到的是一個query對象,上面說過很多后面可以接上的方法,實際上還有更多如下:

  one()  如果返回行數不為1,那么就報錯;若剛好返回結果就一條就返回這條記錄的對象

  limit(n)  最多只返回n條結果

  offset(n)  直接跳過前n條記錄,從n+1條開始返回

  order_by(Table.attribute 或者 'attribute')  返回結果按照給出的字段排序。

  order_by(User.name.desc()) 或者 order_by('name desc')

  filter(condition1).filter(condition2)  多個拼接的filter就相當於and_(condition1,condition2...)

  請注意以上所有方法都要在all()之前調用,all出來已經是一個列表了,不能在作為調用方調用這些方法了。

  還可以些數據庫自帶的函數,在用之前記得from sqlalchemy import func,就可以通過func來調用了。這些函數不是放在調用鏈中,大多數時候都是放在query方法的參數位置,

  比如func.count()  統計一共有多少條記錄作為結果等等

  更多異想天開的方法和更多進階技巧也可以參考這篇文章:【http://www.jb51.net/article/49789.htm】

 

■  更復雜的查詢

  實際開發中經常用到的是連接查詢,反映在SQL語句里面就是含有JOIN的查詢語句。而在ORM模型中,我們可以通過調用join方法來進行連接操作。比如:

students = session.query(Student).join(Class).filter(Class.level == 3).all()
for student in students:
  print stduent.name

   介於都快忘了什么是連接操作,再來復習一下。如果有一張students和一張classes表,通過students.class_id和classes.id兩個形成外鍵,這樣子我們在想要得到students.id和classes.name之間的關系時就可以按照下面的SQL語句來操作:

SELECT students.id,classes.name from students,classes where students.class_id = classes.id;

  如果沒有后面那個where子句的話,得到的是students.id和classes.name形成的笛卡爾積。可以認為這個語句是先得出這個笛卡爾積然后用where條件來篩選結果的。另一種獲得這種關系的語句就是用到了所謂的內連接:

SELECT students.id,classes.name FROM students INNER JOIN classes on students.class_id = classes.id;

  用了一個INNER JOIN,讓students和classes兩表間形成連接,而on子句則是指定了哪些是外鍵。這種連接也是內連接,最為簡單的連接的一種。至於什么是外連接,等/不等值連接,左連接,右連接,自然連接等等請參看數據庫概論。。這里不多說了。

  按照這種連接的思路,如何把SQL轉換成python語句呢?做法就像上面一樣,其實print session.query(Student).join(Class)就可以看到這句語句背后的SQL:

print session.query(orm.Student.name,orm.Class.name).join(orm.Class)

####結果####
SELECT students.name AS students_name, classes.name AS classes_name
FROM students INNER JOIN classes ON classes.id = students.class_name

  可以看到,ORM自動識別了兩表間的外鍵連接,不用我們再去手動指定,所以on后面的子句是不用管的。如果不需要自動設置外鍵或者需要手動指定關聯的外鍵,那么需要在join方法中手動指定。比如:

session.query(orm.Student.name,orm.Class.name).join(orm.Class,orm.Class.level == orm.Student.level)

 

■  關於flask_sqlalchemy的一些補充

  大言不慚在最上面說了不如學原生的。。然而flask項目中用了一下之后發現flask_sqlalchemy也還是不錯的。特此補充一下。一些基本的概念就不說了,直接說一下怎么用吧。

  和其他flask的拓展模塊一樣,在簡單的項目中,初始化sqlalchemy可以這樣操作:

from flask import Flask
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://weiyz:123456@192.168.1.105:3306/flask_DB'
app.config['SQLALCHEMY_COMMIT_ON_TEARDOWN'] = True

db = SQLAlchemy(app)

 

 

  和flask_bootstrap以及flask_moment這些插件不同的是,這里的db不是創建完就似乎沒什么用了,之后的ORM操作基本上都是基於它的。比如和原生SQLAlchemy類似的,要用python類來建立表模型的時候:

####models.py#####

from . import db

class Student(db.models):
    __tablename__ = 'students'
    sno = db.Column(db.String(10),primary_key=True)
    sname = db.Column(db.String(20),nullable=False)
    

db.create_all()

 

  可以看到,和原生的相比,每個操作前都要寫上db了,這主要是為了給ORM操作一些上下文的信息,其他的參數什么的基本上和原生的是一致的。而且比較方便的是不用在create_engine什么的了,直接一步SQLAlchemy(app)搞定。當然這之前要在app的config字典里面配置好該配置的東西,比如這里就寫出了必須的兩樣,URI和斷開連接前自動commit的設置。不過需要說明的是,省事是省事了,但是靈活性自然也就差了。比如之前想要往數據庫中存點中文數據,但是無論如何接受不了除了latin-1(默認編碼)格式以外格式的數據。如果是走原生的SQLAlchemy的話估計就可以自己設定新表的編碼格式。

  在具體的數據操作方面,增刪操作是一樣的,就是用db.session.add和db.session.delete操作一個對象來進行。改的話雖然也是在對象身上直接改屬性的值就行,但是需要經過add再commit才能使生效。而查操作是完全不同的。可以看到在原生的SQLAlchemy中,查詢操作是基於session.query(表類對象)來實現的,但是在flask_sqlalchemy中,查詢操作被包裝得更加完善,所有表類對象都被賦予了直接query的權限。也就是說查詢操作變得像下面這樣:

  Student.query.filter_by(name='Frank').first()

  Student.query.all()

  ……

  過濾器方法除了filter_by還有filter,limit,offset,order_by,group_by等。這些過濾器方法返回的都是一個新查詢

  結果獲取方法除了first還有all,first_or_404,get,get_or_404,count


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM