SQLAlchemy技术手册

一、ORM 框架简介

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

二、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:[email protected]/sqlalchemy', echo=True)
engine.table_names() # 返回数据库中所有表的名字
  • echo参数为True时,会显示每条执行的SQL语句,可以关闭。
  • create_engine()返回一个Engine的实例,它通过数据库语法处理细节的核心接口,数据库语法将会被解释成python的类方法。
  • create_engine() 的第一个参数是 url
    • 例如:sql://fxq:[email protected]/sqlalchemy
    • mysql: 指定是哪个数据库连接。实际上由dialaect[+driver]组成,在默认情况下由名字判断driver
    • xq: 用户名
    • 123456: fxq用户对应的密码
    • 192.168.100.101: 数据库的ip
    • sqlalchemy: 数据库需要连接库的名字
  • 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:[email protected]/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:[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) # 是来创建表,这个操作是安全的操作,会先判断表是否存在。

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

三、 通过 connection 操作 数据

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……
# e.g."mysql://scott:tiger@localhost/test

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)

2 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()) #返回列名组成的列表

自行建立数据库中不存在的表,在第二章的6.2中已经提到过,过程如下:

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

engine = create_engine('mysql+pymysql://fxq:[email protected]/sqlalchemy')
metadata = MetaData()

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)

3 基本的 SQL querying 结构

  • 如果使用 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'])  
  • 结合上面说的事务,一条最基本的查询语句结构如下:
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'])

4 Filtering, Ordering and Grouping

Filter

  • 使用 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): #SQLAlchemy的特性,可以直接使ResultProxy作为循环的目标
    print(result.state, result.pop2000)
  • 连词的使用:and_(),not_(),or_()

    注意使用前需要导入算子,and_()or_() 方法也可以使用 |& 算子来实现,

    记得不同条件要用括号括起来

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

Counting, Summing and Grouping

  • 聚合函数的功能集成在func 模块中,注意不要直接 import sum()…… ,会与Python的内置函数冲突
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() #返回结果是list
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的列

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)

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

6 Creating and Manipulating Data

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 #求出一共添加了多少组数据

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)

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。将上面的 your query expression 替换成 sql expression 即可。下面主要介绍通过session操作模型类的方法。但只有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:[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()

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:[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)

结果:

MariaDB [sqlalchemy]> select * from student;
+---------+---------------+------+---------+
| id      | name          | age  | address |
+---------+---------------+------+---------+
|    1002 | molin         |   18 | jiangxi |
|    1003 | karl          |   16 | suzhou  |
|  100011 | fengxiaoqing  |   18 | chengde |
|  100021 | fengxiaqing1  |  181 | chengde |
| 1000111 | fengxiaoqing  |   18 | chengde |
| 1000211 | fengxiaqing1  |  181 | chengde |
| 1000311 | fengxiaoqing2 |  182 | chengde |
+---------+---------------+------+---------+
7 rows in set (0.00 sec)

MariaDB [sqlalchemy]> select * from student;
+---------+---------------+------+---------+
| id      | name          | age  | address |
+---------+---------------+------+---------+
|    1002 | fengxiaoqing  |   18 | chengde |
|    1003 | karl          |   16 | suzhou  |
|  100011 | fengxiaoqing  |   18 | chengde |
|  100021 | fengxiaqing1  |  181 | chengde |
| 1000111 | fengxiaoqing  |   18 | chengde |
| 1000211 | fengxiaqing1  |  181 | chengde |
| 1000311 | fengxiaoqing2 |  182 | chengde |
+---------+---------------+------+---------+
7 rows in set (0.00 sec)

MariaDB [sqlalchemy]> 

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:[email protected]/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:[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

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:[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))

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:[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>

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

请注意以上所有方法都要在all()之前调用,get()除外。

  • 还可以些数据库自带的函数,在用之前记得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()

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:[email protected]/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:[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
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:[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

7 通过 session+model 多表查询

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')                             # 注意,这里是模型类名
    
    __repr__(self):
      return self.name
    
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')                                             # 注意,这里是模型类名
    
    __repr__(self):
      return self.address

经过上面的对表的定义,数据库中的users和address两张表就通过外键有了联系,为了利用好这种联系,我们就可以灵活运用User类中的addresses属性和Address类中的user属性了。

在类内直接调用relationship定义的属性,就可以直接访问所链接到的模型类的__repr__(self)方法的返回结果。

name = session.query(Address).filter_by(id=xxx).first().user  
# 可以根据address_id查到对应的user_name  
# 因为User类的__repr__(self)方法返回的就是name属性

name = session.query(Address).filter_by(id=xxx).first().user.name
# 当然也可以直接按照属性查找,效果是相等的

如果想通过一个名字直接搜到他的所有邮箱地址,那么就可以直接调用属性:

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

神奇的一点是,SQLAlchemy会根据关系的对应情况自动给关系相关属性的类型。比如这里的User下面的addresses自动是一个list类型,而Address下面的user由于设定了外键的缘故,一个地址最多只能应对一个用户,所以自动识别成一个单一对象类型。

2 使用 backref 指明 relationship

  上面的例子中,我们分别为User和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)
    
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',backref='address')           # 注意,这里是模型类名
关于relationship方法的一些参数

  relationship方法除了传递一个类名和可以加上backref之外,还有一些其他的参数。比如还有uselist=True/False这样一个参数。让uselist成为False之后,所有原先因为有多条结果而成为list类型的关系属性,比如上面例子中的User类下的addresses这个属性,会变成一个单个的变量。同时对于确实是有多个结果的情况系统会给出一个警告。总之,让uselist=False之后可以让这个关系每次通过关系来调用数据的时候都是fetchone而不是fetchall。

  relationship还有另外一个参数,就是lazy。这个参数更加复杂一些。一般而言,就像上面的例子一样,我们通过关系属性来通过一个表的查询对象查询另一个表中的数据,形式上来说是直接.addresses就得到结果了。这是lazy="select"(默认是select)的结果。有时候,直接这么获得的数据比较多比较大,比如一个人有成千上万个邮箱地址的话,或许我不想这么直白地列出所有数据,而是对这些数据做一个二次加工。这时候就要用到lazy="dynamic"模式。在这个模式下,.addresses得到的仍然是一个query对象,所以我们可以进一步调用filter之类的方法来缩小范围。为了提高lazy的使用正确性,SQLAlchemy还规定了,不能再多对一,一对一以及uselist=False的这些模式的关系中使用。

3 多对多关系

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

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

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 

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=='[email protected]').all()

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

for u, count in session.query(User, stmt.c.address_count).\
    outerjoin(stmt, User.id==stmt.c.user_id).order_by(User.id):
    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)

3.使用别名(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=='[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]

4. 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  

# 官方文档
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

猜你喜欢

转载自www.cnblogs.com/sablier/p/11829623.html