资讯详情

SQLAlchemy 1.3文档中文版 - 对象关系指南

SQLAlchemy 1.3文档中文版 - 对象关系指南(Object Relational Tutorial)

中文翻译:郭夫子(3744)jetko@sina.com)

英文原文地址:https://docs.sqlalchemy.org/en/13/orm/tutorial.html

SQLAlchemy对象关系映射器(Object Relational Mapper)它提供了一种定义用户的方法Python类和数据库的表以及这些类的实例(对象)与相应表中的行相关的方法。它包括一个名字unit of work该系统在对象及其相关行之间透明同步状态变化。它还包括另一个系统,根据用户定义类与类之间的关系查询数据库。

ORM不同于SQLAlchemy表达式语言(Expression Language),ORM是基于SQLAlchemy表达式语言构建的。SQLAlchemy表达语言(在SQL Expression Language Tutorial解释)提供的机制直接显示关系数据库的原始结构,无需修改ORM它提供了一种高级抽象的使用模式,由表达式语言构建。

尽管ORM和表达语言的使用模式有重叠,乍看起来二者很相似,实际不然。一是用户定义domain model处理数据的结构和内容。domain model底层存储模型storage model持久性和刷新透明度。另一种是文字schema和SQL表达角度处理。这些表达式由数据库逐个组合成新闻。

只能使用成功的应用程序ORM对象关系映射器射器。在高级情况下使用ORM在某些需要特定数据库交互的区域,构建的应用程序偶尔会直接使用表达式语言。

采用以下教程doctest格式,即 >>> 行表示可以在Python命令提示下键入的内容,随后的文本表示预期的返回值。


Version Check版本检查

快速检查,确认我们至少使用它SQLAlchemy1.3版本:

>>> import sqlalchemy >>> sqlalchemy.__version__ 1.3.0 

Connecting连接

在本教程中,我们将使用只存在于内存中的一个SQLite数据库。我们用 create_engine()来连接:

>>> from sqlalchemy import create_engine >>> engine = create_engine('sqlite:///:memory:', echo=True) 

echo 标志是设置SQLAlchemy日志的快捷方式, 它是利用Python标准库 logging 模块实现。启用后,我们将看到所有生成SQL显示。如果您正在阅读本教程并希望生成更少的输出,请将其设置为 False 。本教程将格式化SQL把它放在弹窗后面,这样它就不会碍眼了;单击SQL链接可以查看正在生成的内容。

create_engine()的返回值是 Engine一个例子, 根据控制数据库细节的方言,它表示数据库的核心接口dialect和在用的DBAPI适配。本例中SQLite方言提供解释指令Python内置的sqlite3模块。

Lazy Connecting懒连接 第一次被create_engine()返回的Engine, 实际上还没有连接到数据库;它只有在第一次被要求执行数据库的任务时才能连接。 

Engine.execute() 或者Engine.connect() 第一次调用这种方法时,Engine真正建立在数据库中的数据库DBAPI然后用于发送连接SQL。当使用ORM时, 我们通常不直接使用它来创建它Engine; 就像我们即将看到的,Engine是被ORM幕后使用。

参见

Database Urls - 包含create_engine()链接到各种数据库的示例和更多信息。


Declare a Mapping声明一个映射

使用ORM在配置过程中,首先描述我们要处理的数据库表,然后定义类,类将被映射到这些表中。SQLAlchemy这两个任务都被称为Declarative通常一起执行机制。Declarative这样我们就可以创建包含指令的真实数据库表,这些指令描述该映射到。

Declarative机制映射类参照一个基类来定义。该基类维护与该基类相关的一系列类和表。这个基类是。在正常导入的模块中,我们的应用程序通常只有一个基类的例子。我们使用它declarative_base()如下:

>>> from sqlalchemy.ext.declarative import declarative_base  >>> Base = declarative_base() 

由于我们已经有了一个基本类别,我们可以根据它来定义任何数量的映射类别。我们将从存储我们应用程序的终端用户的单表开始。我们映射到一个名字User新类别。在这一类中,我们定义表的细节,主要是表名、列名和数据类型:

>>> from sqlalchemy import Column, Integer, String >>> class User(Base): ... __tablename__ = 'users' ... ... id = Column(Integer, primary_key=True) ... name = Column(String) ... fullname = Column(String) ... nickname = Column(String) ... ... def __repr__(self): ... return "<User(name='%s', fullname='%s', nickname='%s')>" % ( ... self.name, self.fullname, self.nickname) 
小提示
User 类定义了一个 __repr__()方法, 注意这是可选的; 我们只在本教程中这样做,为的是我们的示例能漂亮地显示格式化过的User对象。

一个使用Declarative创建的类至少需要一个__tablename__属性,和至少一列Column。该列是主键的一部分。SQLAlchemy自身从不对类指向的表做推测,并且它没有内置的对名字、数据类型或约束条件的规范。但这不意味着需要样例;相反,它鼓励你利用helper辅助函数和mixin类去创建自己的自动化规范。这些会在Mixin and Custom Base Classes中详细介绍。

当类创建时,Declarative把所有的Column对象替换为特殊的Python访问器-描述符。这个过程被称为instrumentaion插桩。被插桩的映射类可提供给我们在一个SQL上下文中引用表以及持久化和从库中加载列中的值的手段。

除开映射过程对我们的类做的这些动作,这个类仍是个正常的Python类,我们可以对它定义任意多的普通属性和方法。

想了解为何需要主键,请看How do I map a table that has no primary key?.


Create a Schema创建schema

对于通过Declarative机制创建的User类,我们已经定义了表的信息,即表的元数据metadata。SQLAlchemy中表示特定表的这些信息的对象叫Table对象,Declarative已经给我们做了一个。我们可以通过考察__table__属性来看看这个对象:

>>> User.__table__ 
Table('users', MetaData(bind=None),
            Column('id', Integer(), table=<users>, primary_key=True, nullable=False),
            Column('name', String(), table=<users>),
            Column('fullname', String(), table=<users>),
            Column('nickname', String(), table=<users>), schema=None)

当我们声明类时,Declarative使用了一个Python元类以便在类声明完成后执行额外的动作;在这个阶段,它按照我们的定义创建了Table对象,并创建了一个Mapper对象,用MapperTable对象跟类关联起来。Mapper对象是个幕后英雄,正常情况下我们不需要直接跟它打交道(尽管它可以在我们需要之时提供丰富的有关映射的信息)。

Table对象是一个更大的集合Metadata的一员。当使用Declarative时,使用Declarative基类的.metadata属性时,这个对象可用。

metadata是个注册器,它能发送一套有限的schema生成指令到数据库。因为我们的SQLite数据库实际上还没有users表,我们可以使用Metadata来发送CREATE TABLE语句到数据库来创建所有还不存在的表。下面,我们调用MetaData.create_all()方法,传进去Engine作为数据库连接的源。我们将看到首先发出了特别的指令检查users表是否存在,跟着才是真正的CREATE TABLE语句:

>>> Base.metadata.create_all(engine
)
SELECT ...
PRAGMA main.table_info("users")
()
PRAGMA temp.table_info("users")
()
CREATE TABLE users (
    id INTEGER NOT NULL, name VARCHAR,
    fullname VARCHAR,
    nickname VARCHAR,
    PRIMARY KEY (id)
)
()
COMMIT
Classical Mappings 经典映射
尽管强烈推荐使用Declarative机制,但它不是使用SQLAchemy ORM所必需的。Declarative之外,直接利用mapper()函数可以将任意纯Python类映射到任意的表;这种不常见的用法在经典映射Classical Mappings中有描述。

最小表描述和完整描述

熟悉CREATE TABLE语法的用户可能会注意到VARCHAR列没有定义length;在SQLite 和PostgreSQL上,这是合法的数据类型,但在其他数据库上不允许。所以,如果是在那些数据库上跑本教程,你想使用SQLAlchemy发出CREATE TABLE的话,要给String数据类型一个length值,像下面一样:

Column(String(50))

String的length字段,以及类似的Integer,Numeric等的precision/scale字段,除了在创建表时用到,SQLAlchemy不会使用。

另外,Firebird和Oracle需要序列来生成新的主键标识符,而sqlAlchemy在没有得到指示的情况下不会生成或推测这些标识符。为此,您使用 Sequence 结构:

from sqlalchemy import Sequence
Column(Integer, Sequence('user_id_seq'), primary_key=True)

因此一个通过Declarative映射生成的,完整的,万无一失的Table是这样:

class User(Base):
    __tablename__ = 'users'
    id = Column(Integer, Sequence('user_id_seq'), primary_key=True)
    name = Column(String(50))
    fullname = Column(String(50))
    nickname = Column(String(50))

    def __repr__(self):
        return "<User(name='%s', fullname='%s', nickname='%s')>" % (
                                self.name, self.fullname, self.nickname)

我们单独列出这个更详细的表定义,是为了突出最小构造和有更严格需求的表构造的区别。最小构造经过调整,主要用于Python程序内;而更严格的表构造用于在特定后端上发送CREATE TABLE语句。


Create an Instance of the Mapped Class创建映射类的实例

映射建好了,我们现在创建和查看User对象:

>>> ed_user = User(name='ed', fullname='Ed Jones', nickname='edsnickname')
>>> ed_user.name
'ed'
>>> ed_user.nickname
'edsnickname'
>>> str(ed_user.id)
'None'

尽管我们没有在构造器中指定id属性,在我们访问它时,它仍给出了一个值None(这与Python正常行为不同,正常访问一个未定义属性会抛出AttributeError异常)。SQLAlchemy的instrumentation插桩正常会在列映射的属性第一次被访问时给它生成这个缺省值。对于我们实际上已经指定了值的属性,instrumentation机制跟踪那些指定值,用在最终的INSERT语句中,发送给数据库。

__init__() 方法


我们用Declarative机制定义的类,已经有了一个构造器(即__init__()方法)。该方法自动接收匹配到列的关键字名称。我们还可自由定义自己喜欢的任意的显式__init__()方法,这将覆盖Declarative提供的缺省方法。

Creating a Session创建会话

我们现在已经准备好跟数据库对话了。ORM与数据库的抓手是Session。在我们最初创建应用时,在create_engine()语句同级别,我们定义一个Session类。该类是创建新Session对象的工厂:

>>> from sqlalchemy.orm import sessionmaker
>>> Session = sessionmaker(bind=engine)

某些情况下如果你定义模块级别的对象时,你的应用还没有Engine,只要这样做:

>>> Session = sessionmaker()

然后,当你用create_engine()创建引擎时,用configure()把它连接到Session:

>>> Session.configure(bind=engine)  # engine可用时

这个定制的Session类会创建新的Session对象,新对象会绑定到数据库。调用sessionmaker时,也可以定义其他的事务性特性。后续章节会讲这些。然后,每当你需要跟数据库对话时,你就实例化一个Session

>>> session = Session()

上面的Session与启用了SQLite的Engine相关联,但它还未打开任何连接。当第一次用它时,它从Engine维护的连接池中取出一个连接,保持住,直到我们提交所有变更并/或关闭会话对象。

Session Lifecycle Patterns会话生命期模式


何时创建会话这个问题很大程度上取决于正在创建的应用的类型。记住,Session只是你的对象的工作空间,一个特定数据库连接的本地空间。如果你把一个应用线程当成饭局上的一个来客,Session就是客人的餐盘,它盛的对象就是食物(数据库是厨房?)!关于此话题更多信息,可访问"When do I construct a Session, when do I commit it, and when do I close it?"

Adding and Updating Objects添加和更新对象

为了持久化User对象,我们add()它到Session

>>> ed_user = User(name='ed', fullname='Ed Jones', nickname='edsnickname')
>>> session.add(ed_user)

现在,我们说实例是进程发送SQL来持久化EdJones。如果查询数据库的Ed Jones记录,所有的pending信息将先被flush,随后查询会立即发出。

例如,下面我们创建一个新的Query对象,该对象载入User实例。我们用name属性为ed来过滤,并说明我们只想要完整记录的第一个结果。返回结果是个User实例,跟我们刚添加的一样:

>>> our_user = session.query(User).filter_by(name='ed').first() 
>>> our_user
<User(name='ed', fullname='Ed Jones', nickname='edsnickname')>

实际上,Session确认了返回的行记录跟它的内部对象映射中的是同一行,所以我们事实上取回了我们刚刚添加的那同一个实例:

>>> ed_user is our_user
True

此处起作用的ORM机制叫同一性映射(identity map),它保证了在一个Session内对某特定行的所有操作操作的都是同一组数据。一旦带有特定主键的对象存在于Session中,该会话上所有针对该主键的SQL查询会返回同一个Python对象;如果试图在会话中加入第二个主键相同且已持久化的对象,机制会抛出异常。

我们可以马上用add_all()加入更多的User对象:

>>> session.add_all([
...     User(name='wendy', fullname='Wendy Williams', nickname='windy'),
...     User(name='mary', fullname='Mary Contrary', nickname='mary'),
...     User(name='fred', fullname='Fred Flintstone', nickname='freddy')])

我们觉得Ed的昵称不太酷,让我们改掉:

>>> ed_user.nickname = 'eddie'

Session一直在关注着,比如,它知道了Ed Jones被改了:

>>> session.dirty
IdentitySet([<User(name='ed', fullname='Ed Jones', nickname='eddie')>])

也知道三个新User对象处于pending状态:

>>> session.new  
IdentitySet([<User(name='wendy', fullname='Wendy Williams', nickname='windy')>,
<User(name='mary', fullname='Mary Contrary', nickname='mary')>,
<User(name='fred', fullname='Fred Flintstone', nickname='freddy')>])

我们告诉Session我们想发布对数据库的所有剩余更改,并提交整个过程中一直在进行的事务。我们可以用commit()Session发出UPDATE语句以反映“Ed”的昵称变化,以及INSERT语句反映我们添加的三个新User对象:

>>> session.commit()

commit()把尚存的变更刷入数据库,并提交事务。会话引用的连接资源归还到连接池。此会话的后续操作将在一个事务中进行,新事务会在首次需要时再次申请连接资源。

我们看看Ed的id属性,先前是None,现在有值了:

>>> ed_user.id 
1

Session在库中插入新行后,所有新生成的id和数据库产生的缺省值在实例上就可用了,或是立即可用,或是在第一次访问时加载。在本例中,整行在被访问时重新加载了,因为我们发出commit()后开始了一个新事务。SQLAlchemy缺省在新事务中首次访问前一次事务取得的数据时会进行数据刷新,如此最新的状态才可用。重新加载的级别是可配置的,见Using the Session中所述。

Session Object States 会话对象状态

当User对象在“Session外”、“Session内但无主键”、“真正被插入表中”之间变化时,它对应四个可用的“对象状态”中的三个-暂时(transient)、挂起(pending)、持久(persistent)。搞清楚这些状态和含义很有好处-请一定阅读Quickie Intro to Object States。

Rolling Back回滚

因为Session工作在事务中,我们也可以回滚所做的变更。让我们做两个要回滚的变更;把ed_user的用户名改成Edwardo

>>> ed_user.name = 'Edwardo'

再添加个错误用户fake_user

>>> fake_user = User(name='fakeuser', fullname='Invalid', nickname='12345')
>>> session.add(fake_user)

查询会话,可以看到操作已经刷到当前的事务中了:

>>> session.query(User).filter(User.name.in_(['Edwardo', 'fakeuser'])).all()
[<User(name='Edwardo', fullname='Ed Jones', nickname='eddie')>, <User(name='fakeuser', fullname='Invalid', nickname='12345')>]

回滚,可以看到ed_user的名字已经改回ed,fake_user也从会话中消失:

>>> session.rollback()
>>> ed_user.name

u'ed'
>>> fake_user in session
False

发送一个SELECT来显示数据库的变更:

>>> session.query(User).filter(User.name.in_(['ed', 'fakeuser'])).all()
[<User(name='ed', fullname='Ed Jones', nickname='eddie')>]

Querying查询

Query对象由Sessionquery()方法生成。这个函数接收不定数量的参数,可以是类和描述符的任意组合。下面,我们查看一个加载了User实例的Query。当在迭代上下文中评估时,返回一个User对象的列表:

>>> for instance in session.query(User).order_by(User.id):
...     print(instance.name, instance.fullname)
ed Ed Jones
wendy Wendy Williams
mary Mary Contrary
fred Fred Flintstone

Query也接受被ORM插桩的描述符作为参数。每当多个类实体或基于列的实体作为query()函数的参数时,返回值是元组:

>>> for name, fullname in session.query(User.name, User.fullname):
...     print(name, fullname)
ed Ed Jones
wendy Wendy Williams
mary Mary Contrary
fred Fred Flintstone

Query返回的元组是命名元组,由keyedTuple类提供,可被当做普通Python对象处理。其名称与属性名及类名相同:

>>> for row in session.query(User, User.name).all():
...    print(row.User, row.name)
<User(name='ed', fullname='Ed Jones', nickname='eddie')> ed
<User(name='wendy', fullname='Wendy Williams', nickname='windy')> wendy
<User(name='mary', fullname='Mary Contrary', nickname='mary')> mary
<User(name='fred', fullname='Fred Flintstone', nickname='freddy')> fred

你可以使用lable()控制单个列的名称。lable()在任何ColumnElement派生对象以及映射到ColumnElement派生对象的类属性(如User.name)上都可用:

>>> for row in session.query(User.name.label('name_label')).all():
...    print(row.name_label)
ed
wendy
mary
fred

假设query()调用返回了多个实体,可以用aliased()来控制一个完整实体(如User)的名称:

>>> from sqlalchemy.orm import aliased
>>> user_alias = aliased(User, name='user_alias')

SQL>>> for row in session.query(user_alias, user_alias.name).all():
...    print(row.user_alias)
<User(name='ed', fullname='Ed Jones', nickname='eddie')>
<User(name='wendy', fullname='Wendy Williams', nickname='windy')>
<User(name='mary', fullname='Mary Contrary', nickname='mary')>
<User(name='fred', fullname='Fred Flintstone', nickname='freddy')>

Query的基本操作包括发送LIMIT和OFFSET,利用Python的数组切片可很方便做到,通常还与ORDER BY一起使用:

>>> for u in session.query(User).order_by(User.id)[1:3]:
...    print(u)
<User(name='wendy', fullname='Wendy Williams', nickname='windy')>
<User(name='mary', fullname='Mary Contrary', nickname='mary')>

还有过滤结果操作,可以用filter_by()(加关键字参数)

>>> for name, in session.query(User.name).\
...             filter_by(fullname='Ed Jones'):
...    print(name)
ed

或者用filter()来完成。filter()可以使用更灵活的SQL表达式语言构建。这就让你可以在映射类上使用常规的Python运算符操作类级别的属性。

>>> for name, in session.query(User.name).\
...             filter(User.fullname=='Ed Jones'):
...    print(name)
ed

Query对象是完全生成型的,意思是,大多数方法的调用会返回一个新的Query对象,在新对象上又可以叠加更多条件。例如,用全名“Ed Jones”查询用户“Ed”,你可以调用filter()两次,这跟使用AND条件匹配一样。

>>> for user in session.query(User).\
...          filter(User.name=='ed').\
...          filter(User.fullname=='Ed Jones'):
...    print(user)
<User(name='ed', fullname='Ed Jones', nickname='eddie')>

Common Filter Operators常用筛选器运算符

下面是filter()中一些最常用的运算符汇总:

  • equals:

     `query.filter(User.name == 'ed')`
    
  • not equals:

    query.filter(User.name != 'ed')

  • LIKE:

    query.filter(User.name.like('%ed%'))

    注意
    ColumnOperators.like()生成的LIKE运算符,在某些后端设备上大小写不敏感,在其他设备又是敏感的。对于保证不区分大小写的比较,请使用ColumnOperators.ilike()。
    

锐单商城 - 一站式电子元器件采购平台