Loading

SQLAlchemy技术手册

一、ORM 框架简介

对象-关系映射(Object/Relation Mapping,简称ORM),是随着面向对象的软件开发方法发展而产生的。面向对象的开发方法是当今企业级应用开发环境中的主流开发方法,关系数据库是企业级应用环境中永久存放数据的主流数据存储系统。对象和关系数据是业务实体的两种表现形式,业务实体在内存中表现为对象,在数据库中表现为关系数据。内存中的对象之间存在关联和继承关系,而在数据库中,关系数据无法直接表达多对多关联和继承关系。因此,对象-关系映射(ORM)系统一般以中间件的形式存在,主要实现程序对象到关系数据库数据的映射。

1. 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,你只需要关心对象就好。对象关系映射成功运用在不同的面向对象持久层产品中,

2. 一般的ORM包括以下四部分:

ORM:及Object-Relational Mapping,把关系数据库的表结构映射到对象上

  • 一个对持久类对象进行CRUD操作的API;
  • 一个语言或API用来规定与类和类属性相关的查询;
  • 一个规定mapping metadata的工具;
  • 一种技术可以让ORM的实现同事务对象一同dirty checking, lazy association fetching以及其他优化操作。

二、SQLAlchemy介绍

Python中最有名的ORM架构就是SQLAlchemy,我们主要就是来学习SQLAlchemy的使用

1 安装环境

pip install SQLAlchemy

2 安装mysql

yum install mysql-server mysql
service mysqld restart
sysctmctl restart mysql.service

3 创建数据库

create database sqlalchemy;

4 授权

GRANT ALL PRIVILEGES ON *.* TO 'fxq'@'%' IDENTIFIED BY ‘123456’;

5 初始化连接

from sqlalchemy import create_engine
engine = create_engine('mysql://fxq:123456@192.168.100.101/my_db', echo=True)
engine.table_names() # 返回数据库中所有表的名字
  • echo参数为True时,会显示每条执行的SQL语句,可以关闭。
  • create_engine() 返回一个Engine的实例,它通过数据库语法处理细节的核心接口,数据库语法将会被解释成python的类方法。
  • create_engine() 的第一个参数是 url
    • 例如:mysql://fxq:123456@192.168.100.101/sqlalchemy
    • mysql:指定是哪个数据库连接。实际上由dialaect[+driver]组成,在默认情况下由名字判断driver
    • fxq: 用户名
    • 123456: fxq用户对应的密码
    • 192.168.100.101: 数据库的ip
    • my_db: 数据库需要连接库的名字
  • create_engine() 函数和连接池相关的参数有:
    • -pool_recycle, 默认为 -1,推荐设置为 7200,即如果 connection 空闲了 7200 秒,自动重新获取,以防止 connection 被 db server 关闭。
    • -pool_size=5, 连接数大小,默认为 5,正式环境该数值太小,需根据实际情况调大。
    • -max_overflow=10, 超出 pool_size 后可允许的最大连接数,默认为 10, 这 10 个连接在使用过后,不放在 pool 中,而是被真正关闭的。
    • -pool_timeout=30, 获取连接的超时阈值,默认为 30 秒。

6 表结构与数据结构

6.1 通过 sql语句 来创建表格

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:123456@192.168.100.101/sqlalchemy')
conn = engine.connect()
conn.execute(sql)
engine.connect()  # 表示获取到数据库连接。类似我们在MySQLdb中游标course的作用。

6.2 通过 ORM 创建表格

from sqlalchemy import create_engine, MetaData, Table, Column, Integer, String

engine = create_engine('mysql+pymysql://fxq:123456@192.168.100.101/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) # 是来创建表,这个操作是安全的操作,会先判断表是否存在。

6.3 Column类

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='服务电话')
              )
  • name 列名
  • type 类型,更多类型见 sqlalchemy.types
  • *args 包括Constraint(约束), ForeignKey(外键), ColumnDefault(默认), Sequenceobjects(序列)
  • key 列名的别名,默认None
  • 下面是可变参数 **kwargs
    • primary_key 如果为True,则是主键
    • nullable 是否可为Null,默认是True
    • default 默认值,默认是None
    • index 是否是索引,默认是True
    • unique 是否唯一键,默认是False
    • onupdate 指定一个更新时候的值,这个操作是定义在SQLAlchemy中,不是在数据库里的,当更新一条数据时设置,大部分用于updateTime这类字段
    • autoincrement 设置为整型自动增长,只有没有默认值,并且是Integer类型,默认是True
    • quote 如果列明是关键字,则强制转义,默认False

6.4 Table类

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 各项定义

    • schema: 此表的结构名称,默认None
    • autoload: 自动从现有表中读入表结构,默认False
      • autoload_with: 从其他engine读取结构,默认None
      • include_columns: 如果autoload设置为True,则此数组中的列将被引用,没有写的列明将被忽略,None表示所有都列明都引用,默认为None;即引用所有列
    • mustexist 如果为True,表示这个表必须在其他的python应用中定义,必须是metadata的一部分,默认False
    • useexisting 如果为True,表示这个表必须被其他应用定义过,将忽略结构定义,默认False
    • owner 表所有者,用于Orcal,默认None
    • quote 设置为True,如果表明是SQL关键字,将强制转义,默认False
    • quote_schema 设置为True,如果列明是SQL关键字,将强制转义,默认False
    • mysql_engine mysql专用,可以设置'InnoDB'或'MyISAM'

6.5 模型类

模型类的主要作用是在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')
  • 模型类可以通过迁移来创建数据库中的表结构,但是sqlalchemy不提供这样的功能。可以查询相关的工具。
  • 只有session具备基于模型类操作数据的能力,是有core的connection无法操作模型类。

三、 通过 Transaction 操作 数据

Transaction的主要目的是建立与数据库的会话,它维护你加载和关联的所有数据库对象。它是数据库查询(Query)的一个入口。它会将我们通过ORM写好的sql expression 翻译成sql语句,并传入数据库执行。

Query对象返回的结果是一组同一映射(Identity Map)对象(或者集合)。事实上,集合中的一个对象,对应于数据库表中的一行(即一条记录)。所谓同一映射,是指每个对象有一个唯一的ID。如果两个对象(的引用)ID相同,则认为它们对应的是相同的对象。

1 connection

from sqlalchemy import create_engine
engine = create_engine("URL")
connection = engine.connect()
# URL的结构:dialaect[+driver]://user:password@host:port/dbname
# dialaect:数据库的名称,比如: mysql,oracle,postgresql……

2 Transaction

使用事务和回滚机制防止数据库错误带来意外损失

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)

3 Reflection

读取数据库,构建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()) # 返回列名组成的列表

4 Basic Query Structure

  • 如果使用 sql语句 查询
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'])

5 Filtering, Ordering and Grouping

  • 使用 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)]

6 Counting, Summing and Grouping

  • 聚合函数的功能集成在func 模块中
from sqlalchemy import func

stmt = select([func.sum(census.columns.pop2008)])  # 求和
results = connection.execute(stmt).scalar()  # 注意scalar()只返回单一值

print(results)
>>302876613
  • Group by
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), ……]
  • ResultsProxy 可以直接与pandas交互转换成DataFrame
import pandas as pd
df = pd.DataFrame(results)
df.columns = results[0].keys() # 列名提取作为DataFrame的列

7 Advanced SQLAlchemy Queries

  • 数值计算
# 计算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)
  • Case Statement
    • 接受条件的列表来进行匹配,最终返回一个满足条件匹配的列对象
    • 条件匹配最终以else子句结束,用来处理那些不匹配条件的情况
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,)]
  • Cast Statement
    • 用来进行数据类型的转换
      • 整型转为浮点型方便进行除法运算
      • 字符串转为日期和时间
    • 参数接受列对象或者是表达式,以及目标数据类型
# 求出居住在纽约的总人口占比
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)

8 SQL Relationships

  • 对于已经定义好表间关系的表格,使用SQLAlchemy自动结合两张表
stmt = select([census.columns.pop2008,state_fact.columns.abbreviation])
results = connection.execute(stmt).fetchall()
  • 对于没有预定义表间关系的表格,join接受一个表格以及额外的表达式来解释两张表的关系

    • join子句传入一个布尔表达式来解释两张表是怎样关联的
    • only join rows from each table that can be related between the two columns
    • 不允许在数据类型不同的列间建立关系
  • join 子句紧跟着select() 子句且在任意where()order_bygroup_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()
  • e.g.2
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()
  • 使用分级表
    • 分级表(hierarchical tables):
      • Contain a relationship with themselves
      • 通常用来储存:组织图(organizational charts),地理资料(geographic data),网络(networks)和关系图(relationship graphs)等
    • 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'),……]
  • Dealing with Large ResultSet
    • 当数据量过大时,可能会引起存储空间不够的问题
    • 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()

9 Creating Databases and Tables

  • 对于SQLite,可以直接使用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_readercsv_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 # 求出一共添加了多少组数据
    

10 Updating Date in a Database

  • 使用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)

11 Deleting Date in a Database

  • 使用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 操作 模型类

Session是对transcation的封装,最重要的功能是实现原子操作。要完成数据库查询,就需要建立与数据库的连接。这就需要用到Engine对象。一个Engine可能是关联一个Session对象,也可能关联一个数据库表。一旦任务完成 session 会将数据库 connection 交还给 pool。

1 建立session链接

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操作模型类的方法。

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的上下文中,否则是报错的!

2 添加数据

添加基本上就是生成模型类对象然后添加到数据库表中:

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:123456@192.168.100.101/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()

3 更新数据

更新不需要模型类对象,也不生成模型对象,只返回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:123456@192.168.100.101/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)

5 删除数据

删除其实也是跟查询相关的,直接查出来,调用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:123456@192.168.100.101/sqlalchemy')
DBSession = sessionmaker(bind=engine)
session = DBSession()

session.query(Student).filter(Student.id == 1001).delete()
session.commit()
session.close()

6 query查询

6.1 filter_by() 函数

通过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:123456@192.168.100.101/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:123456@192.168.100.101/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

6.2 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:123456@192.168.100.101/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))

6.3 filter() 和 filter_by() 的区别

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()

6.4 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:123456@192.168.100.101/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:123456@192.168.100.101/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>

6.5 query 对象的其他返回方式

除了以上提到的 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()除外。

6.6 统计、分组、排序

这些函数可以结合filter使用也可以直接用。

1 统计 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:123456@192.168.100.101/sqlalchemy')
DBSession = sessionmaker(bind=engine)
session = DBSession()

print(session.query(Student).filter(Student.name.like("%feng%")).count())
2 分组 group_by()
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:123456@192.168.100.101/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
3 反序 desc()
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:123456@192.168.100.101/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

7 模型类之间的关联

1 模型类的relationship属性

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

2 关系(backref/back_populates)

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中使用参数backrefback_populates的优点是当Address.user变化时无需flush和commit(无需提交到数据库)就能立即看到关联属性User.addresses的变化;否则只有Address.user的变化在flush和commit(提交到数据库)后才能看到关联属性User.addresses的变化
  • 这种双向映射的过程在python层完成的,并没有与数据库交互
  • 带有参数back_populatesrelationship必须在两个关联表中成对出现,即【双表显示声明】;
    而带有参数backrefrelationship只需在一个表中指定,将自动在关联表中创建一条对应的relationship,即【单表显示声明,关联表隐式创建】

3 从属关系

同一个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')) # 注意,这里是表名

4 级联行为(cascade)

级联行为主要用于控制当主从表中一个张表数据发生增删时另外几张表相应的数据行为。

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')) # 注意,这里是表名

  • cascade参数的缺省默认值为save-update, merge
  • 可选值delete表示删除一对多关系中一方数据时,同时删除多方关联数据
    • 没有设置delete时:在User表中删除id为7的数据时,其关联的所有Address中数据的user_id都会被设为null(注意:SQLAlchemy对Address发出的是Update语句,这些关联数据并没有被删除,仍保留在数据库中,只是外键user_id都被设为null了)
    • 设置了delete时:在User表中删除id为7的数据时,其关联的所有Address中数据都被删除(注意:SQLAlchemy对Address发出的是Delete语句)
  • 可选值delete-orphan表示删除一对多关系中的主表数据的关联属性时,同时删除其字表关联数据的旧值
    • 没有设置delete-orphan时:修改id为7的User.addresses = [Address(one), Address(two)]时,是将Address中原有的 user_id=7 的旧数据的user_id都设为null(注意:SQLAlchemy对Address发出的是Update语句,没有删除这些旧数据,仍保留在数据库中),然后在Address中新增one、two两条数据(user_id=7)
    • 设置了delete-orphan时:则删除 user_id=7 的旧数据(注意:SQLAlchemy对Address发出的是Delete语句)

在数据库层级设置级联行为,而不是在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

5 加载行为

加载行为主要控制在加载主表数据时,何时加载子表的数据。主要分为 延迟加载(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')) # 注意,这里是表名

5.1 延迟加载(lazy loading)

官方推荐保持lazy的默认设置,只在场景需要较多关联表数据时单独将Query对象设为eager loading

  • 查询一个表时不同时加载其关联表数据,直到访问某个具体对象的关联表属性(某个具体的user的user.addresses属性)时才发出一条额外SQL语句单独获取该具体对象的关联表数据
  • 声明relationship的缺省默认加载方式就是lazy loading,即默认设置了参数lazy=‘select’
  • 优点:不加载场景中不需要的关联表数据
  • 缺点:场景中需要使用关联表数据,为了获取100个个具体对象的关联表数据可能要发出100条额外的SQL语句,效率低
  • 一对多关系中使用lazy loading:先发一条SQL语句查询一方数据(例如query(User)有10条结果),为了获取每个user的address,对每个user分别发出一条SQL(即额外发送了10条SQL)
  • 多对一关系中使用lazy loading:先发一条SQL语句查询多方数据(例如query(Address)有10条结果),为了获取每个address对应的user,根据多方结果集中的外键有几个不同的值(例如这10条结果中共有3个不同的user_id)就发出几条SQL(3个)来查询一方数据
5.2 立即加载(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中加载方式性能都差不多

5.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()

6 多对多关系

在现实问题中还有很多多对多关系,比如老师和班级,一个老师可能在很多班级任教,而一个班级显然也有很多不同科目的老师。这种就是多对多关系。

在数据库中通常通过关系表来指明多对多关系,模型类中也一样。在老师和班级的关系中,在老师和班级表中都不能设置外键,一旦设置外键就表明这个每一行这个字段的值只能对应一个值了,又变回一对多关系。总之多对多关系可以像下例中一样构造:

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 

7 多对多关系中lazy属性的使用

在下面的多对多例子中,我们可以看到上述的 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 '<Student: %r>' %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 '<Class: %r>' %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
<sqlalchemy.orm.dynamic.AppenderBaseQuery at 0x1d572c33710>

>>> s1._classes.all()
[<Registration 1, 1>, <Registration 1, 2>]

可见现在 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)

<Class: '语文'>
<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

img

我们可以发现: s1._class 不仅查询了对应的 class信息,而且通过 join 操作,获取到了相应的 Student 和 Class对象。换句话说,把 Registration 的 _student_class 两个回引属性均指向了对应的对象。

8 联表查询

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

Query.join() 知道如何在 StudentClass 之间进行连接,因为我们设定了外键。

假如我们没有指定外键,比如这样:

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=='jack@google.com').all()
8.2 子查询(subquery)

现在需要查询每个用户所拥有的邮箱地址数量,思路是先对 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)
8.3 使用load_only指定加载字段

在连表查询时也可以通过指定字段了指定加载哪些数据

指示对于特定实体,只应加载基于列的属性名的给定列表;所有其他属性名都将被延迟。

为指定子查询加载 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)
)
8.4 使用contain_eager在查询时多次过滤数据
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
)
8.5 使用别名(aliased)

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=='jack@google.com').\
    filter(adalias2.email_address=='j25@yahoo.com'):
    
    print(username, email1, email2)

# 执行结果
jack jack@google.com j25@yahoo.com

上述代码查询同时拥有两个名为:"jack@google.com" 和 "j25@yahoo.com" 邮箱地址的用户。

别名也可以在子查询里使用:

from sqlalchemy.orm import aliased

stmt = session.query(Address).\
    filter(Address.email_address != 'j25@yahoo.com').\
    subquery()

adalias = aliased(Address, stmt)

for user, address in session.query(User, adalias).\
    join(adalias, User.addresses):

    print(user)
    print(address)

# 执行结果
jack
jack@google.com
8.6 EXISTS 关键字

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
posted @ 2019-11-10 13:22  sablier  阅读(3813)  评论(1编辑  收藏  举报