一篇入门SQLAlchemy

changelog

  • [2019-05-06 16:49] 创建文档

背景

近期团队内部拟培训 SQLAlchemy,但我觉得组织培训太费资源了,只是入门教程的话,还是用文档+代码说话吧。所以有了这篇文章,相信工程师们一点就通,不懂的请留言/互相探讨切磋/自行根据文档尝试/google尝试等。

历史

有关关系型数据库和编程,我们知道几点:

  1. SQL 是一种编程语言,专用于数据库查询;Python 也是一种编程语言。
  2. 不同的编程语言需要由不同的后端来执行。SQL 需要有相应的数据库后端来执行,Python 则需要相应的解释器来执行。
  3. Python 解释器可以直接调用 SQL 执行后端提供的 API,但稍微了解数据库的同学就知道,涉及到的细节操作还挺多的(例如编码、网络等)。所有了若干 Python 包,用户可以直接用这些包提供的高级一点的 API 来和数据库打交道(而不用处理许多细节)。
  4. 准确地说,SQL 更像一种规范,即「官话」、「普通话」,有许多方言比如「北京话」、「东北话」等——MySQL,PostgreSQL,Oracle 等等,它们的共性也有各自的特性,如语法、如数据类型。
  5. 不同的方言需要不同的 Python 包来「翻译」和执行,比如 PostgreSQL 常用的 Python 后端是 psycopg2

由上可知,我们在服务某些客户时将遇到这种情况:(不同的)客户的数据存储在不同的数据库中,如 A 客户有 MySQL,B 客户有 Oracle,我们给他们提供的服务相同,只是数据库不同;但我们希望只写一套 Python 代码,通过少数配置项的修改(而非逐一检查所有 SQL 并修改!),就能使这套代码同时运行在两个数据库上。这时候,我们的 ORM 就出场了。

本文仅简单介绍 SQLAlchemy(以下简称 SA)

基本用法

截止文档撰写时,1.3 版本的 SA 是最新的稳定版,因此本文档主要基于 1.3 版撰写而成,主要参考材料是官方文档 https://docs.sqlalchemy.org/en/13/和项目实践

套路:4步法

对于初学者而言,最核心的文档就是这篇 https://docs.sqlalchemy.org/en/13/orm/tutorial.html,根据需要查询 API 即可。SA 使用套路主要就是以下 4 步:

  1. 照着关系型数据库的表结构,定义一个映射(mapping)关系 M
  2. 创建一个引擎(engine)和一个会话(session)
  3. 将会话(session)绑定到引擎(engine)上
  4. 使用会话(session)和映射(M)进行查询

定义映射

有 2 种定义方式:https://docs.sqlalchemy.org/en/13/orm/mapping_styles.html

  • 声明法(declarative)
  • 经典法(classical)

除非有特殊需要,否则一般使用声明法即可,就像写一个类一样简单。

以下是一个示例:

Base = declarative_base()
class PaginationCache(Base):
    __tablename__ = 'pagination_cache'    # 对应数据库中的表名
    token = Column('token', String(50), primary_key=True)
    page = Column('page', Integer, primary_key=True)
    content = Column('content', JSONB)

Base.metadata.create_all(sa_engine['db_user'])    # 建表

关于定义映射所需要的数据类型,请见下一小节。

数据类型

一般只要了解基本数据类型即可:https://docs.sqlalchemy.org/en/13/core/type_basics.html

from sqlalchemy.types import Date, String, Float, Numeric, Text

一些特殊的数据类型需要从方言中导入:https://docs.sqlalchemy.org/en/13/core/type_basics.html#vendor-specific-types

from sqlalchemy.dialects.postgresql import ARRAY

更复杂的内容请自行查阅文档(一般用不上):https://docs.sqlalchemy.org/en/13/core/types.html

创建引擎与会话

基本的用法教程中都有,具体参数请自行查阅文档。给出一份示例代码并简要解释若干点:

对于 engine:

  1. 第 1 个位置参数是数据库协议,具体请见 https://docs.sqlalchemy.org/en/13/core/engines.html
  2. creator 参数接受一个函数,该函数返回一个数据库连接。强调:creator 接受的是「函数」,而不是 「函数返回值」
  3. pool_size 限制了同时允许存在的连接数,设为 0 表示不限制连接数
  4. (可选)echo 为 True/"debug" 时,用于打印日志。详情看文档 https://docs.sqlalchemy.org/en/13/core/engines.html#sqlalchemy.create_engine
  5. (可选)json_serializer 不是必要的:当连接 Postgres 且需要使用 JSON/JSONB 类型时,可以将一个自定义的函数传递给该参数,从而在数据传递进数据库前通过该函数进行 dump 操作 https://docs.sqlalchemy.org/en/13/dialects/postgresql.html#sqlalchemy.dialects.postgresql.JSON
def make_sqlalchemy_handlers(db_choice):
    engine = create_engine(
            'postgresql://',
            creator=lambda x=db_choice: connect(x),
            pool_size=0,
            echo=True,
            json_serializer=json_stringify)
    Session = sessionmaker(autoflush=False)
    Session.configure(bind=engine)
    sa_session = Session()
    return engine, sa_session
    

# 用 psycopg2 连接中文数据库的辅助函数
def connect(db_choice):
    dbname = config.get(db_choice, 'database')
    host = config.get(db_choice, 'host')
    port = config.get(db_choice, 'port')
    user = config.get(db_choice, 'user')
    password = config.get(db_choice, 'password')
    conn = psycopg2.connect(
        "dbname='%(dbname)s' user='%(user)s' password='%(password)s' host='%(host)s' port='%(port)s'"
        % {'dbname': dbname, 'user': user, 'password': password, 'host': host, 'port': port}
    )
    return conn

对于 session: https://docs.sqlalchemy.org/en/13/orm/session_api.html#sqlalchemy.orm.session.Session.init

  1. autoflush 保证每次查询时能够自动把新加入的对象刷入数据库
  2. bind 用来绑定会话(session)与引擎(engine)

查询

以上面的 session 和类为例,如下的方法创建了 2 个查询对象

# 同时查多个列
orm_query_obj_0 = sa_session['db_user'].query(
                PaginationCache.content, PaginationCache.page).filter(
                PaginationCache.token == token)

# 同时有多个筛选条件
orm_query_obj_1 = sa_session['db_user'].query(PaginationCache.content).filter(
                PaginationCache.token == token, PaginationCache.page > 2)

先对上述代码做点解释:

  • sa_session['db_user'] 是从上面的 make_sqlalchemy_handlers 返回的 session
  • 在用声明法定义映射关系的前提下(即写了一个类来表达关系型数据库的表结构):
    • 要查什么内容,只要把待查的若干 映射.列名i0, 映射.列名i1, ... 作为参数传给 query(*args) 方法即可
      • 要进行多表联合(join)查询时,如果不是特别复杂,不一定需要定义 relationship,可以直接用隐式 join 查询:session.query(A.x, B.y).filter(A.x == B.x, A.x == 'constant') https://docs.sqlalchemy.org/en/13/orm/tutorial.html#querying-with-joins
    • 要筛选什么内容,只要把上一小点中的那些参数传给 filter(*args) 方法即可
      • 筛选条件中的任何比较使用 Python 语言的原生运算符即可,特殊的运算符(如要把几个条件用逻辑运算符连接起来)请查阅文档如 https://docs.sqlalchemy.org/en/13/core/sqlelement.html 有时候;Python 标准库的 operator 可能会有用 https://docs.python.org/3/library/operator.html

构建好查询对象 q 后:

  • 直接 print(q) 会打印出这个查询对象 q 对应的 SQL;但具体的常量值会以占位符的形式出现而不会打印填充了值以后的SQL。这是对应于上面的 join 查询的查询对象的打印结果: SELECT A.x AS A_x, B.y AS B_y FROM A, B WHERE A.x = B.y AND A.x = %(A_x)s
  • 常规筛选:(更多 API 请查阅文档 https://docs.sqlalchemy.org/en/13/orm/query.html )
    • q.filter(): 相当于 SQL WHERE
    • q.offset(): 偏移 SQL OFFSET
    • q.limit(): 限制数量 SQL LIMIT
    • q.order_by(): 排序 SQL ORDER BY
  • 获取结果:(更多 API 请查阅文档 https://docs.sqlalchemy.org/en/13/orm/query.html )
    • q.all(): 所有结果列表(哪怕是空列表)
    • q.one(): 拿到 1 条结果,若拿不到(查不到)则抛出异常
    • q.first(): 拿到 1 条结果,若拿不到(查不到)则返回 None

写入

基本套路就是:

  • 把值传递给定义好的映射关系,创建若干对象
  • 把对象传给 ORM 会话,由会话提交操作
  • 提交操作失败时,可以使用「回滚」(rollback)操作

有 2 种方法:

  • 批量添加,一次提交
  • 逐个添加,逐次提交

示例代码如下:

# 批量添加对象,一次性提交
for i in range(pages):
    new_page = PaginationCache(
                    token=token,
                    page=i,
                    content=[{"a": i, "b": i*2}, {"a": i+1, "b": (i+1)*2}])
    new_pages.append(new_page)
    
sa_session['db_user'].add_all(new_pages)
sa_session['db_user'].commit()



# 逐个添加对象,逐次提交;附上事务回滚示例
for i in range(pages):
    new_page = PaginationCache(
                    token=token,
                    page=i,
                    content=[{"a": i, "b": i*2}, {"a": i+1, "b": (i+1)*2}])
    sa_session['db_user'].add(new_page)
    try:
        sa_session['db_user'].commit()
    except:
        print('Error when commit pagination cache.')
        print(traceback.format_exc())        # 打印错误栈
        sa_session['db_user'].rollback()     # 消除这次提交,回滚状态

你可能感兴趣的:(一篇入门SQLAlchemy)