对象-关系映射(Object/Relation Mapping,简称ORM),是随着面向对象的软件开发方法发展而产生的。面向对象的开发方法是当今企业级应用开发环境中的主流开发方法,关系数据库是企业级应用环境中永久存放数据的主流数据存储系统。对象和关系数据是业务实体的两种表现形式,业务实体在内存中表现为对象,在数据库中表现为关系数据。内存中的对象之间存在关联和继承关系,而在数据库中,关系数据无法直接表达多对多关联和继承关系。因此,对象-关系映射(ORM)系统一般以中间件的形式存在,主要实现程序对象到关系数据库数据的映射。
面向对象是从软件工程基本原则(如耦合、聚合、封装)的基础上发展起来的,而关系数据库则是从数学理论发展而来的,两套理论存在显著的区别。为了解决这个不匹配的现象,对象关系映射技术应运而生。O/R中字母O起源于"对象"(Object),而R则来自于"关系"(Relational)。几乎所有的程序里面,都存在对象和关系数据库。在业务逻辑层和用户界面层中,我们是面向对象的。当对象信息发生变化的时候,我们需要把对象的信息保存在关系数据库中。
当开发一个应用程序的时候(不使用O/R Mapping),可能会写不少数据访问层的代码,用来从数据库保存,删除,读取对象信息,等等。在DAL中写了很多的方法来读取对象数据,改变状态对象等等任务。而这些代码写起来总是重复的。
如果开你最近的程序,看看DAL代码,肯定会看到很多近似的通用的模式。我们以保存对象的方法为例,传入一个对象,为SqlCommand对象添加SqlParameter,把所有属性和对象对应,设置SqlCommand的CommandText属性为存储过程,然后运行SqlCommand。对于每个对象都要重复的写这些代码。
除此之外,还有更好的办法吗?有,引入一个O/R Mapping。实质上,一个O/R Mapping会为你生成DAL。与其自己写DAL代码,不如用O/R Mapping。用O/R Mapping保存,删除,读取对象,O/R Mapping负责生成SQL,你只需要关心对象就好。对象关系映射成功运用在不同的面向对象持久层产品中,
ORM:及Object-Relational Mapping,把关系数据库的表结构映射到对象上
Python中最有名的ORM架构就是SQLAlchemy,我们主要就是来学习SQLAlchemy的使用
pip install SQLAlchemy
yum install mysql-server mysql
service mysqld restart
sysctmctl restart mysql.service
create database sqlalchemy;
GRANT ALL PRIVILEGES ON *.* TO 'fxq'@'%' IDENTIFIED BY ‘123456’;
from sqlalchemy import create_engine
engine = create_engine('mysql://fxq:[email protected]/my_db', echo=True)
engine.table_names() # 返回数据库中所有表的名字
url
mysql://fxq:[email protected]/sqlalchemy
dialaect[+driver]
组成,在默认情况下由名字判断drivercreate_engine()
函数和连接池相关的参数有:
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
sql = '''create table student(
id int not null primary key,
name varchar(50),
age int,
address varchar(100));
'''
engine = create_engine('mysql+pymysql://fxq:[email protected]/sqlalchemy')
conn = engine.connect()
conn.execute(sql)
engine.connect() # 表示获取到数据库连接。类似我们在MySQLdb中游标course的作用。
from sqlalchemy import create_engine, MetaData, Table, Column, Integer, String
engine = create_engine('mysql+pymysql://fxq:[email protected]/sqlalchemy')
metadata = MetaData(engine)
student = Table('student', metadata,
Column('id', Integer, primary_key=True),
Column('name', String(50), ),
Column('age', Integer),
Column('address', String(10))
)
metadata.create_all(engine)
MetaData 类主要用于保存表结构,连接字符串等数据,是一个多表共享的对象
metadata = MetaData(engine) # 绑定一个数据源的metadata
metadata.create_all(engine) # 是来创建表,这个操作是安全的操作,会先判断表是否存在。
Column不仅仅用在Table类中使用,也用在模型类中使用。
Column.__init__(self, name, type_, *args, **kwargs)
store = Table('store', ModelBase.metadata,
Column('id', Integer, Sequence("yhk_store_id_seq"), primary_key=True),
Column('name', String, nullable=False, doc='商店名称'),
Column('business_id', Integer, ForeignKey('business.id'), doc='所属企业'),
Column('createtime', DateTime, default=datetime.datetime.now),
Column('address', String, doc='商店地址'),
Column('servicetel', String, doc='服务电话')
)
Table.__init__(self, name, metadata, *args, **kwargs)
store = Table('store', ModelBase.metadata,
Column('id', Integer, Sequence("yhk_store_id_seq"), primary_key=True),
Column('name', String, nullable=False, doc='商店名称'),
Column('business_id', Integer, ForeignKey('business.id'), doc='所属企业'),
Column('createtime', DateTime, default=datetime.datetime.now),
Column('address', String, doc='商店地址'),
Column('servicetel', String, doc='服务电话')
)
name 表名
metadata 共享的元数据
*args Column 是列定义,详见下一节Column部分
下面是可变参数 **kwargs 各项定义
模型类的主要作用是在python环境中使用一个类模拟/映射sql中表的结构
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base() # 生成模型类的基类
class User(Base): # 模型类必须通过继承基类来获得metadata
__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')
Transaction的主要目的是建立与数据库的会话,它维护你加载和关联的所有数据库对象。它是数据库查询(Query)的一个入口。它会将我们通过ORM写好的sql expression
翻译成sql语句,并传入数据库执行。
Query对象返回的结果是一组同一映射(Identity Map)对象(或者集合)。事实上,集合中的一个对象,对应于数据库表中的一行(即一条记录)。所谓同一映射,是指每个对象有一个唯一的ID。如果两个对象(的引用)ID相同,则认为它们对应的是相同的对象。
from sqlalchemy import create_engine
engine = create_engine("URL")
connection = engine.connect()
# URL的结构:dialaect[+driver]://user:password@host:port/dbname
# dialaect:数据库的名称,比如: mysql,oracle,postgresql……
使用事务和回滚机制防止数据库错误带来意外损失
from sqlalchemy import create_engine
engine = create_engine("URL")
connection = engine.connect()
trans = connection.begin()
ins = table1.select()
try:
r1 = connection.execute(ins)
connection.execute(table1.insert(), col1=7, col2='this is some data')
trans.commit()
except:
trans.rollback()
raise
当然,还有更加优雅的写法
with engine.begin() as connection:
r1 = connection.execute(ins1)
r2 = connection.execute(ins2)
读取数据库,构建SQLAlchemy表格对象
from sqlalchemy import MetaData, Table
from sqlalchemy import create_engine
engine = create_engine('sqlite:///census.sqlite')
connection = engine.connect()
metadata = MetaData()
# MetaData元数据:存储数据库信息(比如各种表格)的目录
census = Table('census',metadata,autoload=True,autoload_with=engine)
# 第一个参数传入数据库的名称
# 第二个参数autoload,
# 默认为False,此时可以手动定义和添加column对象,
# 若参数设定为True,则自动从数据库中导出column对象,导出的对象可能会替换我们设定的column对象
print(repr(census)) # 使用repr()功能来预览表格的细节,可以查到列名、列数据的类型
print(metadata.tables) # 以字典的形式返回metadata中的所有表格
print(metadata.tables['census']) # 等价于repr(census)
print(census.columns.keys()) # 返回列名组成的列表
from seqalchemy import create_engine
engine = create_engine('sqlite:///census.sqlite')
connection = engine.connect()
stmt = 'SELECT * FROM people'
result_proxy = connection.execute(stmt)
results = result_proxy.fetchall() # 获取全部查询结果
first_row = results[0] # 输出第一行
print(first_row)
>> ('Illinois','M',0,89600,95012)
print(first_row.keys()) # 输出该行数据对应的列名
>>['state','sex','age','pop2000','pop2008']
print(first_row.state) # 输出具体某一列的数值
>>'Illinois'
如果使用SQLAlchemy expression 查询:
创建engine
创建connection
创建metadata
reflection table
选择 query
的方法
execute
& fetchall()
execute
的结果是ResultProxy
,是一个查询结果对象fetchall()
的结果是ResultSet
,是真实的数据值from sqlalchemy import create_engine, MetaData, Table, select
engine = create_engine('sqlite:///census.sqlite')
connection = engine.connect()
metadata = MetaData()
census = Table('census',metadata,autoload=True,autoload_with=engine)
stmt = select([census]) # select的参数是个list,这里仅包含了census一个元素
results = connection.execute(stmt).fetchall() # 结果为ResultSet
print(results[0][0]) # 读取第一行第一列的数据
print(results[0]['column name']) # 读取第一行中的某一个column
metadata = MetaData()
census = Table('census',metadata,autoload=True,autoload_with=engine)
connection = engine.connect()
ins = census.select()
with engine.begin() as connection:
row = connection.execute(stmt).fetchall()
print(row[0][0])
print(row[0]['column name'])
where
方法来进行条件过滤stmt = select([census])
stmt = stmt.where(census.columns.state == 'California')
results = connection.execute(stmt).fetchall()
for result in results:
print(result.state, result.age)
复杂的条件判断(以下全部为columns方法)
in_()
:匹配列表中的值like()
:匹配通配符的部分值between()
:检查返回值是否在提供的区间内stmt = selcet([census])
stmt = stmt.where(census.columns.state.startwith('New'))
for result in connection.execute(stmt):
print(result.state, result.pop2000)
连词的使用:and_(),not_(),or_()
注意使用前需要导入算子,and_()
和or_()
方法也可以使用 |
和 &
算子来实现,
记得不同条件要用括号括起来
e.g.1
from sqlalchemy import or_
stmt = select([census])
stmt = stmt.where(
or_(census.columns.state == 'California', census.columns.state == 'New York')
)
stmt = stmt.where(
(census.columns.state == 'California') | (census.columns.state == 'New York')
)
for result in connection.execute(stmt):
print(result.state, result.sex)
e.g.2
from sqlalchemy import in_
stmt = select([census])
stmt = stmt.where(census.columns.state.in_(states)) # 查找名字在列表states中的州
e.g.3
from sqlalchemy import and_ , or_
stmt = select([census])
stmt = stmt.where(
and_(census.columns.state == 'New York',
or_(census.columns.age == 21,
census.columns.age == 37)
)
) # 查找纽约州年纪为21岁或37岁的数据
order_by()
语句,默认按升序排序,对于字母,即按字母表顺序排序stmt = select([census.columns.state])
stmt = stmt.order_by(census.columns.state)
from sqlalchemy import desc
stmt = stmt.order_by(desc(census.columns.state)) # 降序排序
order_by()
语句中传入多列,用逗号隔开,按传入列的先后顺序排序from sqlalchemy import desc
stmt = select([census.columns.state, census.columns.age])
# 州按升序排序,年龄按降序排序
stmt = stmt.order_by(census.columns.state, desc(census.columns.age))
results = connection.execute(stmt).fetchall()
print(results)
>>
[('Alabama', 85), ('Alabama', 85), ('Alabama', 84), ('Alabama', 84), ('Alabama', 83), ('Alabama', 83), ('Alabama', 82), ('Alabama', 82), ('Alabama', 81), ('Alabama', 81), ('Alabama', 80), ('Alabama', 80), ('Alabama', 79), ('Alabama', 79), ('Alabama', 78), ('Alabama', 78), ('Alabama', 77), ('Alabama', 77), ('Alabama', 76), ('Alabama', 76)]
func
模块中from sqlalchemy import func
stmt = select([func.sum(census.columns.pop2008)]) # 求和
results = connection.execute(stmt).scalar() # 注意scalar()只返回单一值
print(results)
>>302876613
stmt = select([census.columns.sex, func.sum(census.columns.pop2008)]) # 这里显示了两个值
stmt = stmt.group_by(census.columns.sex)
results = connection.execute(stmt).fetchall()
print(results)
>>[('F',153959198),('M',148917415)]
SQLAlchemy 在 ResultSet 中自动为 functions 生成列名
func_#
,比如 count_1
;这样会导致操作困难label()
函数来更改列名print(results[0].keys())
>> ['sex',u'sum_1']
# 更改生成列的列名
stmt = select([census.columns.sex, func.sum(census.columns.pop2008).label('pop2008_sum')])
stmt = stmt.group_by(census.columns.sex)
results = connection.execute(stmt).fetchall()
print(results[0],keys())
>> ['sex','pop2008_sum']
# 多列聚合类似于多类排序,按照传入列的顺序进行聚合
# 求出不同性别下,各年龄段在2008年的总人口数
stmt = select([
census.columns.sex,
census.columns.age,
func.sum(census.columns.pop2008)
])
stmt = stmt.group_by(census.columns.sex, census.columns.age)
results = connection.execute(stmt).fetchall()
print(results)
>>[('F',0,2105442),('F',1,2087705),('F',2,2037280)……]
distinct()
方法:按列中不同的值分类# 求出一共统计了多少个州的数据
stmt = select([func.count(census.columns.state.distinct())])
distinct_state_count = connection.execute(stmt).scalar()
print(distinct_state_count)
>>51
# 打印出统计的各个州的名称
stmt = select([census.columns.state.distinct()])
different_state = connection.execute(stmt).fetchall()
print(different_state)
>>
[('Illinois',), ('New Jersey',), ('District of Columbia',), ('North Dakota',), ('Florida',), ('Maryland',), ('Idaho',), ('Massachusetts',), ('Oregon',), ('Nevada',), ('Michigan',), ('Wisconsin',), ('Missouri',), ('Washington',), ('North Carolina',), ('Arizona',), ('Arkansas',), ('Colorado',), ……]
# 复杂聚合
from sqlalchemy import func
stmt = select([census.columns.state,func.count(census.columns.age)])
stmt = stmt.group_by(census.columns.state)
results = connection.execute(stmt).fetchall()
print(results)
print(results[0].keys())
>>
[('Alabama', 172), ('Alaska', 172), ('Arizona', 172), ('Arkansas', 172), ('California', 172), ('Colorado', 172), ('Connecticut', 172), ('Delaware', 172), ('District of Columbia', 172), ('Florida', 172), ('Georgia', 172), ('Hawaii', 172), ('Idaho', 172), ('Illinois', 172), ('Indiana', 172), ('Iowa', 172), ('Kansas', 172), ('Kentucky', 172), ('Louisiana', 172), ('Maine', 172), ('Maryland', 172), ('Massachusetts', 172), ('Michigan', 172), ('Minnesota', 172), ('Mississippi', 172), ('Missouri', 172), ('Montana', 172), ……]
['state', 'count_1']
func
函数的表达式写出并赋给一个变量,同时完成新增列的命名# 求出每个州2008年的总人数
from sqlalchemy import func
pop2008_sum = func.sum(census.columns.pop2008).label('population')
stmt = select([census.columns.state,pop2008_sum])
stmt = group_by(census.columns.state)
results = connection.execute(stmt).fetchall()
print(results)
>>
[('Alabama', 4649367), ('Alaska', 664546), ('Arizona', 6480767), ('Arkansas', 2848432), ('California', 36609002), ('Colorado', 4912947), ('Connecticut', 3493783), ('Delaware', 869221), ('District of Columbia', 588910), ('Florida', 18257662), ('Georgia', 9622508), ('Hawaii', 1250676), ('Idaho', 1518914), ('Illinois', 12867077), ('Indiana', 6373299), ('Iowa', 3000490), ('Kansas', 2782245), ('Kentucky', 4254964), ('Louisiana', 4395797), ('Maine', 1312972), ('Maryland', 5604174), ('Massachusetts', 6492024), ('Michigan', 9998854), ……]
import pandas as pd
df = pd.DataFrame(results)
df.columns = results[0].keys() # 列名提取作为DataFrame的列
# 计算2000年到2008年之间人数最多的前5个年龄段
from sqlalchemy import desc
stmt = select([
census.columns.age,
(census.columns.pop2008-census.columns.pop2000).label('pop_change')
])
stmt = stmt.group_by(census.columns.age)
stmt = stmt.group_by(desc('pop_change'))
stmt = stmt.limit(5) # 仅返回前5名
results = connection.execute(stmt).fetchall()
print(results)
from sqlalchemy import case,func
# 求纽约2008年的人口数
stmt = select([
func.sum(
case([
(census.columns.state == 'New York',census.columns.pop2008),
else_=0 #如果数据来自纽约,则返回其2008年人口数用以求和,否则返回0
])
)
])
results = connection.execute(stmt).fetchall()
print(results)
>> [(19465159,)]
# 求出居住在纽约的总人口占比
from sqlalchemy import case, cast, Float
stmt = select([
(func.sum(
case([(census.columns.state == 'New York',
censeus.columns.pop2008)],
else_=0)) # 纽约的总人口数
/cast(func.sum(census.columns.pop2008),Float)*100 # 除以2008年的总人口数 *100%
).label('ny_percent')
])
results = connection.execute(stmt).fetchall()
print(results)
>> [(Decimal('6.4267619765'),)]
# 为了方便阅读也可以分开写
NY_pop2008 = func.sum(
case([
(census.columns.state == 'New York',census.columns.pop2008)
],else_=0)
) # 求纽约的人口数
total_pop2008 = cast(func.sum(census.columns.pop2008),Float) # 求总的人口数
stmt = select([NY_pop2008/total_pop2008*100])
percent = connection.execute(stmt).scalar()
print(percent)
stmt = select([census.columns.pop2008,state_fact.columns.abbreviation])
results = connection.execute(stmt).fetchall()
对于没有预定义表间关系的表格,join
接受一个表格以及额外的表达式来解释两张表的关系
join
子句传入一个布尔表达式来解释两张表是怎样关联的join
子句紧跟着select()
子句且在任意where()
,order_by
或group_by()
子句之前
当我们需要创建一个请求,不再从每个列表中选择需要的列,而是同时使用两个表时,使用select_from
语句来实现,join
子句插入其中
e.g.1
stmt = select([func.sum(census.columns.pop2000)])
stmt = stmt.select_from(census.join(state_fact))
stmt = stmt.where(state_fact.columns.circuit_court == '10')
result = connection.execute(stmt).scalar()
stmt = select([func.sum(census.columns.pop2000)])
stmt = stmt.select_from(census.join(state_fact,
census.columns.state == state_fact.colums.name))
stmt = stmt.where(state_fact.columns.census_division_name == 'East South Central')
result = connection.execute(stmt).scalar()
alias()
方法用来对同一张表创建两个不同的名字,即提供了一种通过不同名称来访问同一张表的方法managers = employees.alias()
stmt = select([manager.columns.name.label('manager'),
employees.colums.name.label('employee')])
stmt = stmt.select_from(employees.join(managers,
managers.columns.id == employees.colums.manager))
stmt = stmt.order_by(managers.colums.name)
print(connection.execute(stmt).fetchall())
>>[(u'FILLMORE',u'GRANT'),(u'FILLMORE',u'ADAMS'),……]
fetchmany()
方法允许仅读取部分数据,将需要提取数据的数量传入该方法ResultProxy
# 已经完成以下定义:
# more_results = True,字典state_count用来存储每个州出现的次数,results_proxy是ResultsProxy类型
while more_results:
partial_results = results_proxy.fetchmany(50)
if partial_results == []:
more_results = False
for row in partial_results:
state_count[row.state] += 1
results_proxy.close()
create_engine()
来创建数据库from sqlalchemy import create_engine, Metadata
from sqlalchemy import (Table, Column, String, Integer, Decimal, Boolean)
engine = create_engine(URL)
metadata = Metadata()
employees = Table('employees',metadata,
Column('id', Integer()),
# 设定name字段不允许出现重复值和空值
Column('name', String(255), unique=True, nullable=False),
# 设定salary字段的默认值为100
Column('salary', Decimal(),default=100.00),
# 设定active字段的默认值为True
Column('active', Boolean(),default=True))
metadata.create_all(engine)
# 可以使用.constraints方法来查看表中设定了哪些限制
print(employees.constraints)
from sqlalchemy import insert
# insert()方法接受表名作为参数,插入的数值写在.values()里
stmt = insert(employees).values(id=1,name='Jason',salary=1.00,active=True)
result_proxy = connection.execute(stmt) # 注意insert方法不返回任何行,所以不用调用fetchall
print(result_proxy.rowcount) # .rowcount属性可以查看添加了多少行
# 添加多行的方法:
# 构建一条不带任何值的statement语句,构建一个字典的列表用来存储需要添加的值,然后在connection中同时将列表和语句传给execute()方法作为参数
stmt = insert(employees)
values_list = [
{'id':2, 'name':'Rebecca', 'salary':2.00, 'active':True},
{'id':3, 'name':'Bob', 'salary':0.00, 'active':False}
]
result_proxy = connection.execute(stmt,values_list)
print(result_proxy.rowcount)
>> 2
将CSV文件添加进表格
CSV
模块来建立一个csv_reader
,csv_reader
是一个阅读器对象,可以迭代CSV文件中的行import csv
file = open('XXX.csv')
csv_reader = csv.reader(file)
stmt = insert(census)
values_list = []
total_rowcount = 0
# 使用枚举方法迭代读取csv文件
for idx, row in enumerate(csv_reader):
data = {'state':row[0], 'sex':row[1], 'age':row[2],
'pop2000':row[3], 'pop2008':row[4]}
values_list.append(data)
if idx % 51 == 0:# 验证数据是否添加完整(数据中共统计了52个州,即0-51)
results = connection.execute(stmt,values_list)
total_rowcount += results.rowcount # 求出一共添加了多少组数据
update()
语句来进行更新,语法结构类似于insert()
where()
子句来选择要更新的数据.values()
子句来更新数据的值from sqlalchemy import update
stmt = update(employees)
stmt = stmt.where(employees.columns.id == '3')
stmt = stmt.values(active = True)
results_proxy = connection.execute(stmt)
print(result_proxy.rowcount)
# 更新多条数据
stmt = update(employees)
stmt = stmt.where(employees.colums.active == True)
stmt = stmt.values(active = False, salary = 0.00)
result_proxy = connection.execute(stmt)
print(result_proxy.rowcount)
# 同步更新:从本表或其他表中选出某个数据,用来作为更新的值更新现有表格中的数据
new_salary = select([employees.columns.salary])
new_salary = new_salary.order_by(desc(employees.columns.salary))
new_salary = new_salary.limit(1) # 选出工资最高的值
stmt = update(employees)
stmt = stmt.values(salary = new_salary) # 修改所有数据
result_proxy = connection.execute(stmt)
delete()
语句来执行删除功能where()
子句来确定需要删除的数据from sqlalchemy import delete
delete_stmt = delete(extra_employees)
result_proxy = connection.execute(delete_stmt) #不加任何条件限制,删除说有数据
stmt = delete(employees).where(employees.columns.id == '3')
result_proxy = connection.execute(stmt)
drop()
语句extra_employees.drop(engine)
print(extra_employees.exists(engine))
>> False
drop_all()
语句删除所有表格metadata.drop_all(engine)
print(engine.table_names())
>> []
Session是对transcation的封装,最重要的功能是实现原子操作。要完成数据库查询,就需要建立与数据库的连接。这就需要用到Engine对象。一个Engine可能是关联一个Session对象,也可能关联一个数据库表。一旦任务完成 session 会将数据库 connection 交还给 pool。
ORM通过session与数据库建立连接进行通信,如下所示:
from sqlalchemy.orm import sessionmaker
DBSession = sessionmaker(bind=engine)
session = DBSession()
...your query action...
try:
session.commit()
return query_result
except Exception as e:
session.rollback()
raise e
finally:
session.close()
通过sessionmake方法创建一个Session工厂,然后在调用工厂的方法来实例化一个Session对象。
session是数据库的连接访问方式,模型类(继承自declarative_base()方法生成的基类的类)主要是模拟/映射数据库表结构关系的类。
session也可以用于提交上面讲到的sql expression。下面主要介绍通过session操作模型类的方法。
ORM模型很方便地将数据库中的一条条记录转变成了python中的一个个对象,有时候我们会想当然地把两者完全等同起来,但是不要忘了,两者之间还必须有session这个中间的桥梁。因为有session在中间做控制,所以必须注目对象和记录之间一个状态上的差别。一般而言,一个数据的对象可以有四种不同的和session关联的状态。从代码的流程上看:
from sqlalchemy.orm import sessionmaker
DBSession = sessionmaker(bind=engine)
session = DBSession() # 创建session对象
frank = Person(name='Frank') # 数据对象得到创建,此时为Transient状态
session.add(frank) # 数据对象被关联到session上,此时为Pending状态
session.commit() # 数据对象被推到数据库中,此时为Persistent状态
session.close() # 关闭session对象
print (frank.name) # 此时会报错DetachedInstanceError,因为此时是Detached状态。
new_session = DBSession()
print (new_session.query(Person).get(1).name) # 可以查询到数据
new_session.close()
四个对象的状态分别是上面四种,Transient/Pending/Persistent/Detached。其中需要比较注意的是Detached状态。并不是我在python中创建了一个数据记录的对象我就可以没有限制地访问它,可以看到访问继承自Base类的这么一个对象时必须要使其身处一个session的上下文中,否则是报错的!
添加基本上就是生成模型类对象然后添加到数据库表中:
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
engine = create_engine('mysql+pymysql://fxq:[email protected]/sqlalchemy')
DBsession = sessionmaker(bind=engine)
session = DBsession()
Base = declarative_base()
class Student(Base):
__tablename__ = 'student'
id = Column(Integer, primary_key=True)
name = Column(String(100))
age = Column(Integer)
address = Column(String(100))
student1 = Student(id=1001, name='ling', age=25, address="beijing")
student2 = Student(id=1002, name='molin', age=18, address="jiangxi")
student3 = Student(id=1003, name='karl', age=16, address="suzhou")
session.add_all([student1, student2, student3])
session.commit()
session.close()
更新不需要模型类对象,也不生成模型对象,只返回true或者false。
from sqlalchemy import Column
from sqlalchemy import Integer
from sqlalchemy import String
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
Base = declarative_base()
class Student(Base):
__tablename__ = 'student'
id = Column(Integer, primary_key=True)
name = Column(String(50))
age = Column(Integer)
address = Column(String(100))
engine = create_engine('mysql+pymysql://fxq:[email protected]/sqlalchemy')
DBSession = sessionmaker(bind=engine)
session = DBSession()
# option1: 查出来在一个个更改
my_stdent = session.query(Student).filter(Student.id == 1002).first()
my_stdent.name = "fengxiaoqing"
my_stdent.address = "chengde"
session.commit()
# option2: 使用update方法和更新字典。更改并不返回查询对象,而是返回True或者False
update_dict = {
"name" : "fengxiaoqing",
"address" : "chengde"}
res = session.query(Student).filter(Student.id == 1002).update(update_dict)
student1 = session.query(Student).filter(Student.id == 1002).first()
print(student1.name, student1.address)
删除其实也是跟查询相关的,直接查出来,调用delete()方法直接就可以删除掉。
from sqlalchemy import Column
from sqlalchemy import Integer
from sqlalchemy import String
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
Base = declarative_base()
class Student(Base):
__tablename__ = 'student'
id = Column(Integer, primary_key=True)
name = Column(String(50))
age = Column(Integer)
address = Column(String(100))
engine = create_engine('mysql+pymysql://fxq:[email protected]/sqlalchemy')
DBSession = sessionmaker(bind=engine)
session = DBSession()
session.query(Student).filter(Student.id == 1001).delete()
session.commit()
session.close()
通过Session的query()方法创建一个查询对象。经过查询后会返回一个模型类对象(或多个对象列表)。
这个函数的参数数量是可变的,参数可以是任何类或者是类的描述的集合。下面来看一个例子:
from sqlalchemy import Column
from sqlalchemy import Integer
from sqlalchemy import String
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
Base = declarative_base()
class Student(Base):
__tablename__ = 'student'
id = Column(Integer, primary_key=True)
name = Column(String(50))
age = Column(Integer)
address = Column(String(100))
engine = create_engine('mysql+pymysql://fxq:[email protected]/sqlalchemy')
DBSession = sessionmaker(bind=engine)
session = DBSession()
my_stdent = session.query(Student).filter_by(name="fengxiaoqing2").first()
print(my_stdent)
结果:
<__main__.Student object at 0x032745F0>
前面我们在赋值的时候,我们可以通过实例化一个对象,然后直接映射到数据库中,那我们在查询出来的数据sqlalchemy直接给映射成一个对象了(或者是每个元素为这种对象的列表),对象和我们创建表时候的class是一致的,我们就也可以直接通过对象的属性就可以直接调用就可以了。
from sqlalchemy import Column
from sqlalchemy import Integer
from sqlalchemy import String
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
Base = declarative_base()
class Student(Base):
__tablename__ = 'student'
id = Column(Integer, primary_key=True)
name = Column(String(50))
age = Column(Integer)
address = Column(String(100))
engine = create_engine('mysql+pymysql://fxq:[email protected]/sqlalchemy')
DBSession = sessionmaker(bind=engine)
session = DBSession()
my_stdent = session.query(Student).filter_by(name="fengxiaoqing2").first()
print(my_stdent.id,my_stdent.name,my_stdent.age,my_stdent.address)
结果:
1000311 fengxiaoqing2 182 chengde
from sqlalchemy import Column
from sqlalchemy import Integer
from sqlalchemy import String
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
Base = declarative_base()
class Student(Base):
__tablename__ = 'student'
id = Column(Integer, primary_key=True)
name = Column(String(50))
age = Column(Integer)
address = Column(String(100))
engine = create_engine('mysql+pymysql://fxq:[email protected]/sqlalchemy')
DBSession = sessionmaker(bind=engine)
session = DBSession()
sql = session.query(Student).filter(Student.name.like("%feng%"))
print(sql)
my_student = sql = session.query(Student).filter(Student.name.like("%feng%")).all()
print(my_student)
结果:
SELECT student.id AS student_id, student.name AS student_name, student.age AS student_age, student.address AS student_address
FROM student
WHERE student.name LIKE %s
<__main__.Student object at 0x032745F0>
filter如果不加上all()或firtst()的话。结果就是一个sql语句。
filter函数中的其他过滤操作:
>>> equals:
query(Student).filter(Student.id == 10001)
query(Address).filter(Address.user == None)
>>> not equals:
query(Student).filter(Student.id != 100)
query(Address).filter(Address.user != None)
>>> LIKE:
query(Student).filter(Student.name.like(“%feng%”))
>>> IN:
query(Student).filter(Student.name.in_(['feng', 'xiao', 'qing']))
>>> NOT IN:
query(Student).filter(~Student.name.in_(['feng', 'xiao', 'qing']))
>>> CONTAIN:
query(User).filter(User.addresses.contains(address)) # 筛选包含某地址的用户
>>> AND:
from sqlalchemy import and_
query(Student).filter(and_(Student.name == 'fengxiaoqing', Student.id ==10001))
或者
query(Student).filter(Student.name == 'fengxiaoqing').filter(Student.address == 'chengde')
>>> OR:
from sqlalchemy import or_
query.filter(or_(Student.name == 'fengxiaoqing', Student.age ==18))
filter: 可以像写 sql 的 where 条件那样写 > < 等条件,但使用列名时,需要通过 类名.属性名
的方式。
filter_by: 可以使用 python 的正常参数传递方法传递条件,指定列名时不需要额外指定类名。参数名对应名类中的属性名,但不能使用 > < 等条件。
当使用filter的时候条件之间是使用“==",fitler_by使用的是"="。
user1 = session.query(User).filter_by(id=1).first()
user1 = session.query(User).filter(User.id==1).first()
filter不支持组合查询,只能连续调用filter来变相实现。
filter_by的参数是**kwargs,直接支持组合查询。
q = session.query(IS).filter(IS.node == node and IS.password == password).all()
q = session.query(IS).filter_by(node = node and password = password).all()
all() 返回一个模型类组成的列表
from sqlalchemy import Column
from sqlalchemy import Integer
from sqlalchemy import String
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
Base = declarative_base()
class Student(Base):
__tablename__ = 'student'
id = Column(Integer, primary_key=True)
name = Column(String(50))
age = Column(Integer)
address = Column(String(100))
engine = create_engine('mysql+pymysql://fxq:[email protected]/sqlalchemy')
DBSession = sessionmaker(bind=engine)
session = DBSession()
my_stdent = session.query(Student).filter(Student.name.like("%feng%")).all()
print(my_stdent)
结果:
[<__main__.Student object at 0x031405B0>, <__main__.Student object at 0x030FCA70>, <__main__.Student object at 0x031405F0>]
可以通过遍历列表来获取每个对象。
one() 返回且仅返回一个查询结果。当结果的数量不足一个或者多于一个时会报错。
first() 返回至多一个结果,而且以单项形式。当不足一个时返回为None(推荐)
from sqlalchemy import Column
from sqlalchemy import Integer
from sqlalchemy import String
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
Base = declarative_base()
class Student(Base):
__tablename__ = 'student'
id = Column(Integer, primary_key=True)
name = Column(String(50))
age = Column(Integer)
address = Column(String(100))
engine = create_engine('mysql+pymysql://fxq:[email protected]/sqlalchemy')
DBSession = sessionmaker(bind=engine)
session = DBSession()
my_stdent = session.query(Student).filter(Student.name.like("%feng%")).first()
print(my_stdent)
结果:
<__main__.Student object at 0x030A3610>
除了以上提到的 all() 和 first() 以外,查询对象还有很多的显示方法。
query() 以某些字段作为参数
session.query(Student.id,Student.name).filter(Student.name.like("F%")).all()
# from sqlalchemy.orm import load_only 也可以完成类似的功能
session.query(Student).options(load_only(Student.id,Student.name)).filter(Student.name.like("F%")).all()
one() 如果返回行数不为1,那么就报错;若刚好返回结果就一条就返回这条记录的对象
my_stdent = session.query(Student).filter(Student.id = 151101).one()
one_or_none() 查找一个结果,不存在查询结果时返回None,不会报错
my_stdent = session.query(Student).filter(Student.id = 151101).one_or_none()
get(k) 函数获取固定主键结果
my_stdent = session.query(Student).get(151101)
limit(n) 最多只返回n条结果
session.query(Student).limit(10).all()
offset(n) 直接跳过前n条记录,从n+1条开始返回
session.query(Student).offset(5).all()
order_by(Table.attribute 或者 'attribute') 返回结果按照给出的字段排序。
session.query(Student).limit(10).order_by(Student.id).all()
order_by(User.name.desc()) 或者 order_by('name desc') 返回结果按照给出的字段的降序排序。
session.query(User).filter(User.id > 1).order_by(User.id.desc()).offset(5).limit(3).all()
filter(condition1).filter(condition2) 多个拼接的filter就相当于and_(condition1,condition2...)
session.query(User).filter(User.id > 1).filter(User.id < 10).all()
还可以些数据库自带的函数,在用之前记得from sqlalchemy import func
,就可以通过func来调用了。这些函数不是放在调用链中,大多数时候都是放在query方法的参数位置。
from sqlalchemy import func
session.query(Relation.user_id, func.count(Relation.target_user_id)) .group_by(Relation.user_id).all()
请注意以上所有方法都要在all()之前调用,get()除外。
这些函数可以结合filter使用也可以直接用。
from sqlalchemy import Column
from sqlalchemy import Integer
from sqlalchemy import String
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
Base = declarative_base()
class Student(Base):
__tablename__ = 'student'
id = Column(Integer, primary_key=True)
name = Column(String(50))
age = Column(Integer)
address = Column(String(100))
engine = create_engine('mysql+pymysql://fxq:[email protected]/sqlalchemy')
DBSession = sessionmaker(bind=engine)
session = DBSession()
print(session.query(Student).filter(Student.name.like("%feng%")).count())
from sqlalchemy import Column
from sqlalchemy import Integer
from sqlalchemy import String
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
Base = declarative_base()
class Student(Base):
__tablename__ = 'student'
id = Column(Integer, primary_key=True)
name = Column(String(50))
age = Column(Integer)
address = Column(String(100))
engine = create_engine('mysql+pymysql://fxq:[email protected]/sqlalchemy')
DBSession = sessionmaker(bind=engine)
session = DBSession()
std_group_by = session.query(Student).group_by(Student.age)
print(std_group_by)
std_group_by_list = std_group_by = session.query(Student).group_by(Student.age).all()
for i in std_group_by_list:
print(i.id)
结果的sql语句如下:
SELECT student.id AS student_id, student.name AS student_name, student.age AS student_age, student.address AS student_address
FROM student GROUP BY student.age
1002
100011
100021
1000111
1000211
1000311
from sqlalchemy import Column
from sqlalchemy import Integer
from sqlalchemy import String
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
Base = declarative_base()
class Student(Base):
__tablename__ = 'student'
id = Column(Integer, primary_key=True)
name = Column(String(50))
age = Column(Integer)
address = Column(String(100))
engine = create_engine('mysql+pymysql://fxq:[email protected]/sqlalchemy')
DBSession = sessionmaker(bind=engine)
session = DBSession()
std_ord_desc = session.query(Student).filter(Student.name.like("%feng%")).order_by(Student.id.desc()).all()
for i in std_ord_desc:
print(i.id)
结果:
1000311
1000211
1000111
100021
100011
1002
from sqlalchemy import create_engine,Column,String,Integer,ForeignKey
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker,relationship
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属性了。
在类内直接调用relationship定义的属性,就可以直接获得所关联到的模型信息
user = session.query(User).filter_by(id=xxx).first()
address = user.addresses
address_str = address.address
# 这样可以通过user获得address,进而获得address下的各种属性
如果想通过一个名字直接搜到他的所有邮箱地址,那么就可以直接调用属性:
address = session.query(User).filter_by(name="xxx").first().addresses.address
用from sqlalchemy.orm import backref
的这个backref可以更加方便地一次性写清双向的关系。这就是直接把backref='user'
作为参数添加在addresses = relationship('Adress',backref='user')
中。
from sqlalchemy import create_engine,Column,String,Integer,ForeignKey
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker,relationship, backref
class User(Base):
__tablename__ = 'users'
id = Column(Integer,primary_key=True)
name = Column(String(20),nullable=False)
address = relationship('Address',backref='user') # 注意,这里是模型类名
class Address(Base):
__tablename__ = 'address'
id = Column(Integer,primary_key=True)
address = Column(String(20),nullable=False)
user_id = Column(Integer,ForeignKey('users.id')) # 注意,这里是表名
relationship
中使用参数backref
或back_populates
的优点是当Address.user
变化时无需flush和commit(无需提交到数据库)就能立即看到关联属性User.addresses
的变化;否则只有Address.user
的变化在flush和commit(提交到数据库)后才能看到关联属性User.addresses
的变化back_populates
的relationship
必须在两个关联表中成对出现,即【双表显示声明】;backref
的relationship
只需在一个表中指定,将自动在关联表中创建一条对应的relationship
,即【单表显示声明,关联表隐式创建】同一个User用户可以拥有多个address,反应在关联关系上就是User对象可以关联上多个Address对象。用uselist参数可以指定relationship关联关系时一对一还是一对多的。这对接下来要讨论的主从表关系操作是很重要的。
from sqlalchemy import create_engine,Column,String,Integer,ForeignKey
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker,relationship, backref
class User(Base):
__tablename__ = 'users'
id = Column(Integer,primary_key=True)
name = Column(String(20),nullable=False)
address = relationship('Address',backref='user', uselist=True) # 注意,这里是模型类名
class Address(Base):
__tablename__ = 'address'
id = Column(Integer,primary_key=True)
address = Column(String(20),nullable=False)
user_id = Column(Integer,ForeignKey('users.id')) # 注意,这里是表名
级联行为主要用于控制当主从表中一个张表数据发生增删时另外几张表相应的数据行为。
from sqlalchemy import create_engine,Column,String,Integer,ForeignKey
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker,relationship, backref
class User(Base):
__tablename__ = 'users'
id = Column(Integer,primary_key=True)
name = Column(String(20),nullable=False)
addresses = relationship("Address", backref="user", cascade="all, delete-orphan")
class Address(Base):
__tablename__ = 'address'
id = Column(Integer,primary_key=True)
address = Column(String(20),nullable=False)
user_id = Column(Integer,ForeignKey('users.id')) # 注意,这里是表名
在数据库层级设置级联行为,而不是在sqlalchemy层级设置行为
from sqlalchemy import create_engine,Column,String,Integer,ForeignKey
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker,relationship, backref
class User(Base):
__tablename__ = 'users'
id = Column(Integer,primary_key=True)
name = Column(String(20),nullable=False)
addresses = relationship("Address", backref="user", passive_deletes=True,passive_updates=True)
class Address(Base):
__tablename__ = 'address'
id = Column(Integer,primary_key=True)
address = Column(String(20),nullable=False)
user_id = Column(Integer, ForeignKey('users.id', onupdate="CASCADE", ondelete="CASCADE"))
需在ForeignKey中传入onupdate = “CASCADE”, ondelete =“CASCADE”
同时在relationship中传入passive_deletes=True,passive_updates=True
加载行为主要控制在加载主表数据时,何时加载子表的数据。主要分为 延迟加载(lazy loading) ,立即加载(eager loading) 和动态加载(dynamic)三种,通过lazy参数来指定
from sqlalchemy import create_engine,Column,String,Integer,ForeignKey
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker,relationship, backref
class User(Base):
__tablename__ = 'users'
id = Column(Integer,primary_key=True)
name = Column(String(20),nullable=False)
addresses = relationship("Address", backref="user", cascade="all, delete-orphan", lazy="dynamic")
class Address(Base):
__tablename__ = 'address'
id = Column(Integer,primary_key=True)
address = Column(String(20),nullable=False)
user_id = Column(Integer,ForeignKey('users.id')) # 注意,这里是表名
官方推荐保持lazy的默认设置,只在场景需要较多关联表数据时单独将Query对象设为eager loading
多方关联数据集合较小时使用joinedload,其他情况能用selectinload就用selectinload,用不了就改用subqueryload
查询一个表时同时一次性加载其关联表数据,即查询返回时,关联表属性(所有符合查询条件的user的user.addresses属性)已填充了数据
可以通过在声明relationship时设置lazy=‘joined’、‘subquery’、‘selectin’等值将加载方式指定为eager loading,官方不推荐这样做
官方推荐通过对Query对象设置options来单独将此次查询设置为eager loading
5.2.1 多方关联数据集合较小时使用joinedload
总共1条SQL语句,默认为LEFT OUTER JOIN,可通过传入innerjoin=True提升性能,但innerjoin可能改变一方查询结果
from sqlalchemy.orm import joinedload
user= session.query(User).options(joinedload(User.addresses)).all()
SELECT
users.id AS users_id,
users.name AS users_name,
users.fullname AS users_fullname,
users.nickname AS users_nickname,
addresses_1.id AS addresses_1_id,
addresses_1.email_address AS addresses_1_email_address,
addresses_1.user_id AS addresses_1_user_id
FROM
users
LEFT OUTER JOIN
addresses AS addresses_1 ON users.id = addresses_1.user_id
ORDER BY addresses_1.id;
5.2.2 多方关联数据集合很大时使用subqueryload
总共2条SQL语句,第1条查询user,第2条填充user.addresses,使用INNER JOIN,不改变一方查询结果,且子查询的字段仅为一方表主键
缺点:如果查询中使用了first、limit、offset,则必须同时使用order_by,否则可能产生错误结果
from sqlalchemy.orm import subqueryload
user= session.query(User).options(subqueryload(User.addresses)).all()
SELECT
users.id AS users_id,
users.name AS users_name,
users.fullname AS users_fullname,
users.nickname AS users_nickname
FROM
users;
SELECT
addresses.id AS addresses_id,
addresses.email_address AS addresses_email_address,
addresses.user_id AS addresses_user_id,
anon_1.users_id AS anon_1_users_id
FROM
(SELECT
users.id AS users_id
FROM
users) AS anon_1
INNER JOIN
addresses ON anon_1.users_id = addresses.user_id
ORDER BY anon_1.users_id , addresses.id;
5.2.3 当要同时加载多个不同的多方关联表数据(及需要join多个表)时使用selectinload
总共1+(N / 500)条SQL语句,第1条查询user,第2条(及以后)填充user.addresses,不会产生笛卡尔积问题,不会多次join,无需order_by,性能高
缺点:一个SQL语句一次只能获取多方关联数据集合的500条数据,集合数据量超过500时,将每500个发出一个SQL
缺点:对于复合主键,selectin加载不是平台无关的,已知支持的DBAPI为PostgreSQL, MySQL, SQLite,对于不支持的DBAPI将抛异常
from sqlalchemy.orm import selectinload
user= session.query(User).options(selectinload(User.addresses)).all()
SELECT
users.id AS users_id,
users.name AS users_name,
users.fullname AS users_fullname,
users.nickname AS users_nickname
FROM
users;
SELECT
addresses.user_id AS addresses_user_id,
addresses.id AS addresses_id,
addresses.email_address AS addresses_email_address
FROM
addresses
WHERE
addresses.user_id IN (第1条SQL语句获得的所有user的主键)
ORDER BY addresses.user_id , addresses.id
5.2.4 多对一关系中希望立即加载一方关联数据
由于一方数据是单个的,不是集合,使用上述3中加载方式性能都差不多
将relationship的返回结果设为Query对象,而不是固定的集合。dynamic不能使用在一对一关系中
适用于关联表的结果集非常大的情况, 例如一个user可能有非常多的blogs,在访问User.blogs这个关系时并不希望将所有blogs都加载到一个list中,而是希望能够对blogs进一步过滤,通过lazy="dynamic"将User.blogs设为一个query对象(而不是list),从而可以在查询User.blogs的SQL语句中设置过滤条件,而不是被迫一次性都提取到内存中在程序中过滤
注意:动态关系不能与lazy loading 或 eager loading同时使用,因为动态关系的返回是Query对象,而lazy loading 或 eager loading的返回都是固定的集合(例如:list)
class User(Base):
__tablename__ = 'users'
id = Column(Integer,primary_key=True)
name = Column(String(20),nullable=False)
# 参数lazy="dynamic"将User.blogs这个关系设为动态关系
posts = relationship("BlogPost", back_populates="author", lazy="dynamic")
user = session.query(User).filter_by(id=3).one_or_none()
# 由于设置了lazy="dynamic",则user.posts是Query对象,可以对其进行filter、limit、offset等各种过滤,从而只返回符合过滤条件的blog,而不是此user的所有blog
blog = user.posts.filter_by(headline='funny story').limit(2).all()
在现实问题中还有很多多对多关系,比如老师和班级,一个老师可能在很多班级任教,而一个班级显然也有很多不同科目的老师。这种就是多对多关系。
在数据库中通常通过关系表来指明多对多关系,模型类中也一样。在老师和班级的关系中,在老师和班级表中都不能设置外键,一旦设置外键就表明这个每一行这个字段的值只能对应一个值了,又变回一对多关系。总之多对多关系可以像下例中一样构造:
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
在下面的多对多例子中,我们可以看到上述的 lazy
方式的优势,
我们把关联表改为实体 model
,并且额外增加一个时间信息字段。模型代码如下:
class Registration(db.Model):
__tablename__ = 'registrations'
student_id = db.Column(
db.Integer,
db.ForeignKey('students.id'),
primary_key=True
)
class_id = db.Column(
db.Integer,
db.ForeignKey('classes.id'),
primary_key=True
)
create_at = db.Column(
db.DateTime,
default=datetime.utcnow
)
class Student(db.Model):
__tablename__ = 'students'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64))
_classes = db.relationship(
'Registration',
foreign_keys = [Registration.student_id],
backref = db.backref('_student', lazy='joined'),
lazy = 'dynamic'
)
def __repr__(self):
return '' %self.name
class Class(db.Model):
__tablename__ = 'classes'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64))
_students = db.relationship(
'Registration',
foreign_keys = [Registration.class_id],
backref = db.backref('_class', lazy='joined'
),
lazy='dynamic')
def __repr__(self):
return '' %self.name
手动插入数据:
c1 = Class(id=1, name='语文')
c2 = Class(id=2, name='数学')
s1 = Student(id=1, name='小明')
s2 = Student(id=2, name='小李')
s3 = Student(id=3, name='小红')
r1 = Registration(student_id=3, class_id=1)
r2 = Registration(student_id=2, class_id=2)
r3 = Registration(student_id=1, class_id=1)
r4 = Registration(student_id=1, class_id=2)
db.session.add_all([s1, s2, s3, c1, c2, r1, r2, r3, r4])
db.session.commit()
执行查询操作:
>>> s1 = Student.query.first()
>>> c1 = Class.query.first()
>>> s1._classes
>>> s1._classes.all()
[, ]
可见现在 s1._classes
的调用只返回 Registration 对象,并不返回 Student 和 Class 对象。
我们在 Student 类的声明里面定义了 backref = db.backref('_student', lazy='joined')
,所以可以用 registration._student
来调用对应的 Student 对象;同理要查询 Class 对象可使用 registration._class
。
>>> for i in s1._classes.all():
....: print(i._class)
那么问题就来了,这里在调用 Registration
的 _class
和 _student
时候,还需不需要再查询一遍数据库呢?
下面通过查看执行的 sql 语句来看看。
>>> print(s1._classes)
SELECT registrations.student_id AS registrations_student_id,
registrations.class_id AS registrations_class_id,
registrations.create_at AS registrations_create_at,
students_1.id AS students_1_id,
students_1.name AS students_1_name,
classes_1.id AS classes_1_id,
classes_1.name AS classes_1_name
FROM registrations LEFT OUTER JOIN students AS students_1 ON
students_1.id = registrations.student_id
LEFT OUTER JOIN classes AS classes_1 ON
classes_1.id = registrations.class_id
WHERE l = registrations.student_id
我们可以发现: s1._class
不仅查询了对应的 class信息,而且通过 join 操作,获取到了相应的 Student 和 Class对象。换句话说,把 Registration 的 _student
和 _class
两个回引属性均指向了对应的对象。
students = session.query(Student).join(Class).filter(Class.level == 3).all()
for student in students:
print stduent.name
Query.join()
知道如何在 Student
和 Class
之间进行连接,因为我们设定了外键。
假如我们没有指定外键,比如这样:
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(String(50))
fullname = Column(String(50))
password = Column(String(12))
class Address(Base):
__tablename__ = 'addresses'
id = Column(Integer, primary_key=True)
email_address = Column(String, nullable=False)
user_id = Column(Integer)
我们可以用下面方法来让 join
生效:
query.join(Address, User.id==Address.user_id) # explicit condition
query.join(User.addresses) # specify relationship from left to right
query.join(Address, User.addresses) # same, with explicit target
query.join('addresses') # same, using a string
例子:
session.query(User).\
join(Address, User.id==Address.user_id).\
filter(Address.email_address=='[email protected]').all()
现在需要查询每个用户所拥有的邮箱地址数量,思路是先对 addresses 表按用户 ID 分组,统计各组数量,这样我们得到一张新表;然后用 JOIN 连接新表和 users 两个表,在这里,我们应该使用 LEFT OUTER JOIN,因为使用 INTER JOIN 所得出的新表只包含两表的交集。
from sqlalchemy.sql import func
stmt = session.query(Address.user_id, func.count('*').\
label('address_count')).\
group_by(Address.user_id).subquery()
query = session.query(User, stmt.c.address_count).\
outerjoin(stmt, User.id==stmt.c.user_id).order_by(User.id)
for u, count in query:
print(u, count)
# 执行结果
ed None
wendy None
mary None
fred None
jack 2
1 . 如果上面的暂时看不懂,我们先来看看第一个 stmt 的情况。
from sqlalchemy.sql import func
stmt = session.query(Address.user_id, func.count('*').\
label('address_count')).\
group_by(Address.user_id).all()
for i in stmt:
print(i)
# 执行结果
(5, 2)
2 . 可以理解成 group_by()
方法生成了一张新的表,该表有两列,第一列是 user_id ,第二列是该 user_id 所拥有的 addresses 的数量,这个值由 func()
跟着的方法产生,我们可以使用 c()
方法来访问这个值。
from sqlalchemy.sql import func
stmt = session.query(Address.user_id, func.count('*').\
label('address_count')).\
group_by(Address.user_id).subquery()
q = session.query(User, stmt.c.address_count).\
outerjoin(stmt, User.id==stmt.c.user_id).order_by(User.id).all()
for i in q:
print(i)
# 执行结果
(ed, None)
(wendy, None)
(mary, None)
(fred, None)
(jack, 2)
如果不用 outerjoin()
而使用 join()
,就等于使用 SQL 中的 INTER JOIN,所得出的表只为两者交集,不会包含 None 值的列。
from sqlalchemy.sql import func
stmt = session.query(Address.user_id, func.count('*').\
label('address_count')).\
group_by(Address.user_id).subquery()
q = session.query(User, stmt.c.address_count).\
join(stmt, User.id==stmt.c.user_id).order_by(User.id).all()
for i in q:
print(i)
# 执行结果
(jack, 2)
在连表查询时也可以通过指定字段了指定加载哪些数据
指示对于特定实体,只应加载基于列的属性名的给定列表;所有其他属性名都将被延迟。
为指定子查询加载 User.addresses
收集,但在每个 Address
对象仅加载 email_address
属性:
session.query(User).options(
subqueryload(User.addresses).load_only(Address.email_address)
)
对于一个 Query 如果有多个实体,则可以使用 Load :
session.query(User, Address).join(User.addresses).options(
Load(User).load_only(User.name, User.fullname),
Load(Address).load_only(Address.email_address)
)
user= session.query(User).options(subqueryload(User.addresses)).filter(User.id = xxx).all()
但是在这种情况下,我们无法再执行其他过滤操作。
使用contains_eager
我们可以完成以下操作:
q_manual = (
session
.query(User)
.join(Address) # MUST HAVE THIS
.options(contains_eager(User.address))
.filter(User.id == 99)
.filter(Address.address == "No.1 street")
.order_by(Address.id, User.id) # JUST ADD THIS to solve the ordering
)
SQLAlchemy 使用 aliased()
方法表示别名,当我们需要把同一张表连接多次的时候,常常需要用到别名。
from sqlalchemy.orm import aliased
# 把 Address 表分别设置别名
adalias1 = aliased(Address)
adalias2 = aliased(Address)
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=='[email protected]').\
filter(adalias2.email_address=='[email protected]'):
print(username, email1, email2)
# 执行结果
jack [email protected] [email protected]
上述代码查询同时拥有两个名为:"[email protected]" 和 "[email protected]" 邮箱地址的用户。
别名也可以在子查询里使用:
from sqlalchemy.orm import aliased
stmt = session.query(Address).\
filter(Address.email_address != '[email protected]').\
subquery()
adalias = aliased(Address, stmt)
for user, address in session.query(User, adalias).\
join(adalias, User.addresses):
print(user)
print(address)
# 执行结果
jack
[email protected]
EXISTS 关键字可以在某些场景替代 JOIN 的使用。
from sqlalchemy.sql import exists
stmt = exists().where(Address.user_id==User.id)
for name, in session.query(User.name).filter(stmt):
print(name)
# 执行结果
jack
使用 any()
方法也能得到同意的效果:
for name, in session.query(User.name).\
filter(User.addresses.any()):
print(name)
使用 any()
方法时也可加上查询条件:
for name, in session.query(User.name).\
filter(User.addresses.any(Address.email_address.like('%google%'))):
print(name)
使用 has()
方法也能起到 JOIN 的作用:
session.query(Address).filter(~Address.user.has(User.name=='jack')).all()
注意:这里的 ~
符号是 “不” 的意思。
# Engines and Connections
https://www.jianshu.com/p/e6bba189fcbd # 简易版
https://www.jianshu.com/p/d1fea79027f3 # 详尽版
# session
https://www.jianshu.com/p/aa1241c41ef3 # 操作目录
https://www.jianshu.com/p/0ad18fdd7eed # 更加详细
https://www.cnblogs.com/franknihao/p/7268752.html # 更加进阶
# relationship
https://www.jianshu.com/p/8427da16729a/
https://blog.csdn.net/u012324798/article/details/103940527
# 官方文档
https://docs.sqlalchemy.org/en/14/core/tutorial.html # SQL Expression Language Tutorial
https://docs.sqlalchemy.org/en/14/core/metadata.html # Describing Databases with MetaData
https://docs.sqlalchemy.org/en/14/core/type_basics.html # Column and Data Types
https://docs.sqlalchemy.org/en/14/core/connections.html # Working with Engines and Connections
https://docs.sqlalchemy.org/en/14/orm/session.html # Using the Session
https://docs.sqlalchemy.org/en/14/dialects/postgresql.html # PostgreSQL