SQLAlchemy學習筆記(四)

建立關系

操作數據庫中的表,表之間不能只是獨立存在而和其他的表沒有任何的依賴關系,所以我們接下來學習SQLAlchemy中的關系。

讓我們考慮如何映射和查詢與用戶相關的第二個表。我們系統中的用戶可以存儲任意數量的與其用戶名相關聯的電子郵件地址。這意味著從用戶到存儲電子郵件地址的新表的基本一對多關聯,我們稱之為地址。使用聲明,我們定義這個表及其映射類Address:

>>> from sqlalchemy import ForeignKey
>>> from sqlalchemy.orm import relationship
>>> class Address(Base):
...     __tablename__ = 'addresses'
...     id = Column(Integer, primary_key=True)
...     email_address = Column(String, nullable=False)
...     user_id = Column(Integer, ForeignKey('users.id'))
...     user = relationship("User", back_populates="addresses")
...     def __repr__(self):
...             return "<Address(email_address='%s')>" % self.email_address

上面的類引入了ForeignKey結構,它是一個應用于Column的指令,表示列中的值是參照與其他的表中的某一列。這也是關系數據庫的核心特性,就是我們說的外鍵約束。上面的ForeignKey表示,addresses.user_id列中的值應該參照users.id列中的值,就是參照users的主鍵。

從類定義中還使用了一個新的函數,稱為 relationship(),告訴ORM,Address類本身應該使用屬性Address.user鏈接到User類。 relationship()使用兩個表之間的外鍵關系來確定此連接的性質,確定Address.user將是多對一的關系。在用戶映射類的屬性User.addresses下放置一個附加relationship()指令。在relationship()指令中,參數relationship.back_populates被分配以引用補充屬性名稱;通過這樣做,每個relationship()可以做出與反向表示相同關系的智能決策;Address.user引用一個User實例,User.addresses引用一個Address實例的列表,因為用戶和地址是一對多的關系。

Note

relationship.back_populates參數是一個非常常見的SQLAlchemy功能的一個較新版本,稱為relationship.backref。relationship.backref參數沒并沒有丟棄,將始終保持可用。

關于外鍵

  • 大多數但是不是全部的關系型數據庫中的FOREIGNKEY約束只能連接到主鍵列或者具有UNIQUE約束的列。
  • 引用多列主鍵并且自身具有多個列的FORENGN KEY約束稱為"復合外鍵"。
  • FOREIGN KEY列可以自動更新自己,以響應引用列或行中的更改。這被稱為CASCADE參考動作就是級聯,并且是關系數據庫的內置函數。
  • FOREIGN KEY可以參考自己的表。這被稱為“自引用”外鍵,就是有一些表不是依賴其他的表,而是參照自身的。

下面我們來創建我們的addresses

 Base.metadata.create_all(ngine)
2017-03-10 17:20:26,394 INFO sqlalchemy.engine.base.Engine select version()
2017-03-10 17:20:26,395 INFO sqlalchemy.engine.base.Engine {}
2017-03-10 17:20:26,398 INFO sqlalchemy.engine.base.Engine select current_schema()
2017-03-10 17:20:26,398 INFO sqlalchemy.engine.base.Engine {}
2017-03-10 17:20:26,401 INFO sqlalchemy.engine.base.Engine SELECT CAST('test plain returns' AS VARCHAR(60)) AS anon_1
2017-03-10 17:20:26,401 INFO sqlalchemy.engine.base.Engine {}
2017-03-10 17:20:26,403 INFO sqlalchemy.engine.base.Engine SELECT CAST('test unicode returns' AS VARCHAR(60)) AS anon_1
2017-03-10 17:20:26,404 INFO sqlalchemy.engine.base.Engine {}
2017-03-10 17:20:26,405 INFO sqlalchemy.engine.base.Engine show standard_conforming_strings
2017-03-10 17:20:26,407 INFO sqlalchemy.engine.base.Engine {}
2017-03-10 17:20:26,408 INFO sqlalchemy.engine.base.Engine select relname from pg_class c join pg_namespace n on n.oid=c.relnamespace where pg_catalog.pg_table_is_visible(c.oid) and relname=%(name)s
2017-03-10 17:20:26,410 INFO sqlalchemy.engine.base.Engine {'name': u'addresses'}
2017-03-10 17:20:26,417 INFO sqlalchemy.engine.base.Engine select relname from pg_class c join pg_namespace n on n.oid=c.relnamespace where pg_catalog.pg_table_is_visible(c.oid) and relname=%(name)s
2017-03-10 17:20:26,417 INFO sqlalchemy.engine.base.Engine {'name': u'users'}
2017-03-10 17:20:26,421 INFO sqlalchemy.engine.base.Engine
CREATE TABLE addresses (
        id SERIAL NOT NULL,
        email_address VARCHAR NOT NULL,
        user_id INTEGER,
        PRIMARY KEY (id),
        FOREIGN KEY(user_id) REFERENCES users (id)
)


2017-03-10 17:20:26,426 INFO sqlalchemy.engine.base.Engine {}
2017-03-10 17:20:27,009 INFO sqlalchemy.engine.base.Engine COMMIT

使用相關的對象

現在我們創建一個User用戶時,會出現一個空的地址addresses集合,這是因為我們給User新增加了一個屬性叫做addresses,默認情況下這個集合是一個python列表。

>>> jack = User(name='jack', fullname='Jack Bean', password='gjfdd')
>>> jack.addresses
[]  # 訪問User對象的addresses屬性,可以看到是一個list類型,這是因為還沒有任何的地址對象。

下面我們給這個列表添加兩個地址對象

>>> jack.addresses = [Address(email_address='jack@qq.com'), Address(email_address='j12@gamil.com')]

當使用雙向關系時,在一個方向上添加的元素自動在另一個方向上可見。不使用任何SQL語句就可以實現:

>>> jack.addresses[1]
<Address(email_address='j12@gamil.com')>
>>> jack.addresses[1].user
<User(name=jack, fullname=Jack Bean, password=gjfdd)>

從上面我們可以看見,直接使用User的addresses屬性添加的地址對象,再通過地址對象訪問User對象也是能訪問的。

讓我們將Jack用戶添加并提交到數據庫。 jack以及相應地址集合中的兩個Address成員都使用稱為級聯的過程同時添加到會話:

>>> session.add(jack)
>>> session.commit()

現在來查詢數據庫看看:

>>> jack = session.query(User).filter(User.name=='jack').one()
2017-03-10 19:16:59,690 INFO sqlalchemy.engine.base.Engine BEGIN (implicit)
2017-03-10 19:16:59,690 INFO sqlalchemy.engine.base.Engine SELECT users.id AS users_id, users.name AS users_name, users.fullname AS users_fullname, users.password AS users_password
FROM users
WHERE users.name = %(name_1)s
2017-03-10 19:16:59,690 INFO sqlalchemy.engine.base.Engine {'name_1': 'jack'}
>>> jack
<User(name=jack, fullname=Jack Bean, password=gjfdd)>

查詢到jack用戶后,看看是否有地址信息

>>> jack.addresses
2017-03-10 19:17:15,247 INFO sqlalchemy.engine.base.Engine SELECT addresses.id AS addresses_id, addresses.email_address AS addresses_email_address, addresses.user_id AS addresses_user_id
FROM addresses
WHERE %(param_1)s = addresses.user_id ORDER BY addresses.id
2017-03-10 19:17:15,247 INFO sqlalchemy.engine.base.Engine {'param_1': 4}
[<Address(email_address='jack@qq.com')>, <Address(email_address='j12@gamil.com')>]
>>>

我們可以從上面打印的信息看出,在執行jack.addresses時,才執行了SQL查詢語句。這就是延遲加載關系。

使用連接(joins)查詢

現在我們已經有了兩個表,我們可以展示一些Query的新特性,特別是如何處理兩個表的查詢。

要在UserAddress之間構造一個簡單的隱式連接,我們可以使用Query.filter()來將它們相關的列等同在一起。下面我們使用這種方法一次加載用戶和地址實體。

>>> for u, a in session.query(User, Address).filter(User.id==Address.user_id).filter(Address.email_address=='jack@qq.com').all():
...     print u
...     print a
...
2017-03-10 19:48:39,792 INFO sqlalchemy.engine.base.Engine SELECT users.id AS users_id, users.name AS users_name, users.fullname AS users_fullname, users.password AS users_password, addresses.id AS addresses_id, addresses.email_address AS addresses_email_address, addresses.user_id AS addresses_user_id
FROM users, addresses
WHERE users.id = addresses.user_id AND addresses.email_address = %(email_address_1)s
2017-03-10 19:48:39,795 INFO sqlalchemy.engine.base.Engine {'email_address_1': 'jack@qq.com'}
<User(name=jack, fullname=Jack Bean, password=gjfdd)>
<Address(email_address='jack@qq.com')>
>>>

從運行日志可以看出其實就是轉化成SQL語句:

SELECT users.id, name, password, adddresses.id, email_address, user_id FROM users, addresses
WHERE users.id=addresses.user_id
AND addresses.email_address='jack@qq.com'

我們學習過數據庫知道,當對兩個表進行連接查詢時,如果兩個表中列名有重名,需要使用tableName.columnName來做區分,但是使大量使用連接查詢會影響數據庫性能。

另一方面,實際的SQL JOIN語法使用````Query.join()```方法更加容易實現。

>>> session.query(User).join(Address).filter(Address.email_address=='jack@qq.com').all()
2017-03-10 19:53:49,559 INFO sqlalchemy.engine.base.Engine SELECT users.id AS users_id, users.name AS users_name, users.fullname AS users_fullname, users.password AS users_password
FROM users JOIN addresses ON users.id = addresses.user_id
WHERE addresses.email_address = %(email_address_1)s
2017-03-10 19:53:49,559 INFO sqlalchemy.engine.base.Engine {'email_address_1': 'jack@qq.com'}
[<User(name=jack, fullname=Jack Bean, password=gjfdd)>]
>>>

數據庫中我們學習過自然連接,使用join()方法將兩張表連接,,然后對連接后的表進行過濾。由于query()方法中的參數是User,所以連接后的表會抽取指定的列。

Query.join()知道如何將UserAddress進行連接,這是因為他們之間有唯一的外鍵關聯,如果沒有外鍵或者有幾個外鍵,Query.join()想更好的工作需要使用下面的方式:

query.join(Address, User.id==Address.user_id)    # 顯示的指定
query.join(User.addresses)                       # 指定從左到右的關系
query.join(Address, User.addresses)             
query.join('addresses')                          # 使用字符串
疑問?#####

為什么使用字符串就可以?我認為join()方法中的字符串會被轉化成python表達式。

關于數據庫中的多個表格進行連接,分為自然連接、等值連接。自然連接中可能出現懸浮元組,為了避免丟掉懸浮元組,使用外連接將這些懸浮元組中填上NULL。對于只保留左邊的懸浮元組就叫做左連接,同理又有右連接。
左連接我們使用

query.outerjoin(User.addresses)   # LEFT OUTER JOIN
如果有多個實體,Query該怎么選擇?

操作數據庫肯定會遇到同時需要操作多個表格,通常Query.join()選擇最左邊的實體進行連接,要控制Join列表中的第一個實體,使用Query.select_from()方法。

>>> session.query(User, Address).select_from(Address).join(User).filter(Address.email_address=='jack@qq.com').all()
2017-03-10 20:15:59,964 INFO sqlalchemy.engine.base.Engine SELECT users.id AS users_id, users.name AS users_name, users.fullname AS users_fullname, users.password AS users_password, addresses.id AS addresses_id, addresses.email_address AS addresses_email_address, addresses.user_id AS addresses_user_id
FROM addresses JOIN users ON users.id = addresses.user_id
WHERE addresses.email_address = %(email_address_1)s
2017-03-10 20:15:59,964 INFO sqlalchemy.engine.base.Engine {'email_address_1': 'jack@qq.com'}
[(<User(name=jack, fullname=Jack Bean, password=gjfdd)>, <Address(email_address='jack@qq.com')>)]
>>>

這個方法主要是控制連個表連接時在前面,就是誰是主動連的一方,使用上面的語句,主動連接的一方是Address。

使用別名

當我們查詢多個表時,如果相同的表要使用多次,SQL通常需要該表使用另一個名稱進行別名,以便可以區分該表與其他出現的表。下面我們加入到地址實體兩次,以找到同時有兩個不同電子郵件地址的用戶:

>>> from sqlalchemy.orm import aliased
>>> adalias1 = aliased(Address) # 別名1
>>> adalias2 = aliased(Address) # 別名2
>>> for username, email1, email2 in \
... session.query(User.name, adalias1.email_address, adalias2.email_address).\
... join(adalias1, User.addresses).\
... join(adalias2, User.addresses).\
...filter(adalias1.email_address=='jack@google.com').\
...filter(adalias2.email_address=='j25@yahoo.com'):
... print username, email1, email2
jack jack@google.com j25@yahoo.com

使用EXISTS

SQL中的EXISTS關鍵字是一個布爾運算符,如果給定的表達式包含任何行,則返回True。它可以在許多情況下用于代替聯接,并且也用于定位在相關表中沒有對應行的行。

>>> from sqlalchemy.sql import exists
>>> stmt = exists().where(Address.user_id==User.id)
>>> for name in session.query(User.name).filter(stmt):
...     print name
...
2017-03-10 20:30:51,213 INFO sqlalchemy.engine.base.Engine SELECT users.name AS users_name
FROM users
WHERE EXISTS (SELECT *
FROM addresses
WHERE addresses.user_id = users.id)
2017-03-10 20:30:51,213 INFO sqlalchemy.engine.base.Engine {}
(u'jack',)

今天先學到這了,繼續學沒有效率!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容