11.6 添加ORM层

    有许多有关Python的ORM项目,从https://wiki.python.org/moin/HigherLevelDatabase

    Programming可以找到一个列表。

    我们会选择其中的一个作为例子,这时我们选择 SQLAlchemy,因为它提供给我们许多功能而且它的使用相对广泛。正如其他事物一样,没有最好的选择,毕竟其他的 ORM也都有各自的优缺点。

    由于在Web开发中关系数据库使用的广泛性,Web框架通常会自带ORM层。Django有它自己的ORM层,web.py也是如此。在一些情况下,可以将ORM移到框架的外面,使用一个独立的ORM会更简便一些。

    有关SQLAlchemy的文档、安装说明和代码可以在这里找到:http://www.sqlalchemy.org。在安装时,如果对很高的性能没有特殊要求,可使用—without-cextensions来简化安装过程。

    SQLAlchemy可以完全将应用中的SQL语句替换为Python中的第1级构造函数,这会带来显著的优势。我们可以只使用一种语言即Python来编写应用程序,尽管第2种语言(SQL)被使用了,毕竟它也只是数据访问层的一部分。如此一来,还会大幅度降低开发和调试的复杂度。

    然而,它并不意味着可以不必理解SQL数据库中的约束,在设计时依然需要考虑到这些约束。ORM层并不意味着设计上什么都不用考虑,它只是将SQL中的实现移到了Python中。

    11.6.1 设计ORM友好的类

    当使用ORM时,需要在根本上改变设计的方法并且实现持久化类。类定义将会包括3种不同层次的含义。

    • 类可以是一个Python类,用于创建Python对象。方法函数由这些对象来使用。
    • 类也可以用于描述SQL表,可被ORM用于创建SQL DDL语句,完成数据库结构的新建和维护。
    • 类也定义了SQL表和Python类之间的映射。它将完成将Python操作转换为SQL DML并基于SQL查询创建Python对象。

    大多数ORM的设计使得我们需要使用修饰符来正式地定义类中的属性。我们不会简单地将属性的定义放在init()方法中。有关修饰符的更多信息,可以看第3章“属性访问、特性和修饰符”。

    SQLAlchemy需要创建一个定义性的基类(declarative base class)。这个基类为应用的类定义提供了元类。对于元数据而言它是我们为数据库所定义的库。默认地,很容易将这个类称为Base

    以下是一个可能有用的导入列表。

    from sqlalchemy.ext.declarative import declarative_base
    from sqlalchemy import Column, Table
    from sqlalchemy import BigInteger, Boolean, Date, DateTime, Enum, \
      Float, Integer, Interval, LargeBinary, Numeric, PickleType, \
      SmallInteger, String, Text, Time, Unicode, UnicodeText ForeignKey
    from sqlalchemy.orm import relationship, backref

    我们导入了一些基本的定义,用于创建表中的列以及创建一些表,它们并没有指定用于完成Python类和表之间的映射。我们导入了所有通用的列类型的定义,实际只会用到其中的一些。SQLAchemy不但定义了这些泛型,它还定义了SQL标准类型,而且还为不同类型的SQL供应商定义了一些特殊类型。坚持使用泛型并在SQLAlchemy中完成泛型、标准和供应商类型的转换不算很复杂。

    我们还导入了两个helper来定义表之间的关系:relationshipbackref。SQLAlchemy的元类由declarative_base()函数来创建。

    Base = declarative_base()

    所创建的Base对象必须是将要定义的持久化类的元类。我们会定义3个表,它们会映射为Python类。我们也会定义第4张表,被SQL用来实现多对多的关系。

    以下是Blog类。

    class Blog(Base):
      tablename = "BLOG"
      id = Column(Integer, primary_key=True)
      title = Column(String)
      def as_dict( self ):
        return dict(
          title= self.title,
          underline= '='*len(self.title),
          entries= [ e.as_dict() for e in self.entries ]
        )

    Blog类被映射为了一张名称为“BLOG”的表。在这张表中,为两个列加了修饰符。id列被定义为一个Integer主键。它是自增的,因此代理主键就会自动生成。

    标题列被定义为一个泛型字符串,有关这种类型,我们用过TextUnicode,甚至UnicodeText。对于不同的类型,底层引擎可能提供了不同的实现。对SQLite而言,这些几乎是相同的。并且可以注意到,SQLite不需要对一个列加最大长度限制,其他数据库引擎可能会需要为String的长度提供上限。

    as_dict()引用了一个entries集合,它并没有在类中定义。观察一下Post类的定义,可以看到entries属性是如何创建的,以下是Post类的定义。

    class Post(Base):
      tablename = "POST"
      id = Column(Integer, primary_key=True)
      title = Column(String)
      date = Column(DateTime)
      rst_text = Column(UnicodeText)
      blog_id = Column(Integer, ForeignKey('BLOG.id'))
      blog = relationship( 'Blog', backref='entries' )
      tags = relationship('Tag', secondary=assoc_post_tag,
    backref='posts')
      def as_dict( self ):
        return dict(
          title= self.title,
          underline= '-'*len(self.title),
          date= self.date,
          rst_text= self.rst_text,
          tags= [ t.phrase for t in self.tags],
        )

    这个类有5个属性、两个关系和一个方法函数。id属性是一个整型的主键,默认情况下,它将是一个自增的值。title属性是一个简单的字符串。date属性是一个DateTime列。rst_text 被定义为 UnicodeText,用于强调我们期望这个字段中的任何字符都是Unicode的。

    blog_id是一个外键,引用包含了这篇文章的父blog。而且,对于外键列的定义,在文章和父博客之间也定义了一个显式relationship。这个relationship的定义将作为一个属性,用于完成从文章到父博客的导航。

    backref选项中包含了向后引用,将被加入Blog类中。这个引用在Blog类中将作为文章集合,包含在Blog中。backref选项命名了Blog类中的新属性,用来引用子Post

    tag属性使用了一个relationship的定义,这个属性将通过一个关联的表来完成此操作:查找和文章相关的所有Tag实例。我们会看一下其他相关的表,在这里也使用了backref来将一个属性包含在Tag类中,它将引用相关Post实例的集合。

    as_dict()方法使用了tags属性来查找与指定Post有关的所有Tag。以下是Tag类的定义。

    class Tag(Base):
      tablename = "TAG"
      id = Column(Integer, primary_key=True)
      phrase = Column(String, unique=True)

    我们定义了一个主键和一个String属性。我们使用了一种约束来确保每个标签是显式唯一的,如果试图向数据库中插入重复记录则会引发异常。在Post类定义中的关系,表明在这个类中会有额外的属性被创建。

    为了SQL中的实现需要,需要定义一个关联表来解决标签和文章之间的多对多关系。这个表只是在SQL中技术上的需要,因此不必添加Python类的映射。

    assoc_post_tag = Table('ASSOC_POST_TAG', Base.metadata,
      Column('POST_ID', Integer, ForeignKey('POST.id') ),
      Column('TAG_ID', Integer, ForeignKey('TAG.id') )
    )

    我们必须显式地将它绑定在Base.metadata集合上。使用了Base作为元类的类,将自动包含这个绑定。我们定义了一个表,包含两个Column实例。每1列都是一个连接到其他表的外键。

    11.6.2 使用ORM层创建模型

    为了连接到数据库,需要创建一个引擎。引擎的一种使用方式是创建数据库实例,其中包含了表的定义。另一种使用场景是管理会话中的数据,接下来我们会介绍。以下是一个用于创建数据库的脚本。

    from sqlalchemy import create_engine
    engine = create_engine('sqlite:///./p2_c11_blog2.db', echo=True)
    Base.metadata.create_all(engine)

    当创建一个Engine实例时,我们使用了一种类似URL的字符串,其中定义了供应商产品并提供所有需要的额外参数来创建数据库连接。对SQLite而言,连接是一个文件名。对于其他数据库产品而言,可能还会包含主机名和认证信息。

    一旦有了引擎,就完成了一些基本的元数据操作。我们实现了 create_all(),其中包括了所有表的创建。可能还会执行一个drop_all(),用于删除所有的表,会移除所有数据。当然,也可以选择创建或移除整个数据库模型。

    如果在软件开发期间修改了表定义,在 SQL 中表的修改并不会自动完成。我们需要显式地移除并重新创建那张表。对于一些情形,我们希望保存一些初始化数据,将旧表数据向新表中添加时会更复杂一些。

    echo=True选项意味着,生成的SQL语句会写入日志记录。当为了确认定义是否已完成,是否创建了正确的数据库模型,这个是有用的。以下是生成的一段输出结果。

    CREATE TABLE "BLOG" (
     id INTEGER NOT NULL,
     title VARCHAR,
     PRIMARY KEY (id)
    )

    CREATE TABLE "TAG" (
     id INTEGER NOT NULL,
     phrase VARCHAR,
     PRIMARY KEY (id),
     UNIQUE (phrase)
    )

    CREATE TABLE "POST" (
     id INTEGER NOT NULL,
     title VARCHAR,
     date DATETIME,
     rst_text TEXT,
     blog_id INTEGER,
     PRIMARY KEY (id),
     FOREIGN KEY(blog_id) REFERENCES "BLOG" (id)
    )

    CREATE TABLE "ASSOC_POST_TAG" (
     "POST_ID" INTEGER,
     "TAG_ID" INTEGER,
     FOREIGN KEY("POST_ID") REFERENCES "POST" (id),
     FOREIGN KEY("TAG_ID") REFERENCES "TAG" (id)
    )

    以上输出表明,数据库基于类而定义,使用CREATE TABLE语句创建了相关的表。

    一旦完成了数据库的创建,我们就可以对对象进行创建、获取、修改和删除操作。为了与数据库对象一起工作,需要创建一个会话,作为ORM托管对象的缓存。

    11.6.3 使用ORM层操作对象

    为了与对象协同工作,需要一个会话缓存,这是绑定在引擎上的。我们有时会向会话缓存中添加新对象,有时也会使用会话缓存查询数据库中的对象。这可以确保所有需要持久化的对象都已经放入了缓存。如下代码演示了创建会话的一种方式。

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

    我们需要SQLAlchemy中的sessionmaker()函数来创建一个Session类。它是绑定在之前所创建的数据库引擎中的。然后使用了Session类来创建session对象,我们使用它来执行数据操作。一般情况下,会话是必需的。

    通常,创建sessionmaker类时,其中会包含引擎。然后就可以使用sessionmaker类来为应用创建多个会话。

    对于简单对象来说,会使用如下代码来创建并将它加载到会话中。

    blog= Blog( title="Travel 2013" )
    session.add( blog )

    以上代码将一个新的Blog对象添加到名为session的会话中。Blog对象不必写入数据库。我们需要在数据库写操作执行前提交会话。为了确保操作的原子性,会在完成创建一篇文章的创建后才提交会话。

    首先,会查找数据库中的Tag实例。如果它们不存在则新建。如果它们存在,将直接使用。

    tags = [ ]
    for phrase in "#RedRanger", "#Whitby42", "#ICW":
      try:
        tag= session.query(Tag).filter(Tag.phrase == phrase).one()
      except sqlalchemy.orm.exc.NoResultFound:
        tag= Tag(phrase=phrase)
        session.add(tag)
      tags.append(tag)

    我们使用session.query()函数来检查指定类中的实例。每个filter()函数会添加一个关键字到查询中。one()函数用来确保已经找到了一行记录。如果有异常抛出,那么则意味着Tag不存在,需要创建新的Tag然后添加到会话中。

    一旦找到或创建了Tag实例,就可以将它添加到名为tags的列表中,并将用这个Tag实例的列表来创建Post对象。以下代码演示了如何创建一个Post

    p2= Post( date=datetime.datetime(2013,11,14,17,25),
      title="Hard Aground",
      rst_text="""Some embarrassing revelation. Including and """,
      blog=blog,
      tags=tags
      )
    session.add(p2)
    blog.posts= [ p2 ]

    它包含了一个到父博客的引用,同时也包括了新建(或找到的)的 Tag 实例的列表。

    在类定义中,Post.blog属性被定义为一个关系。当为一个对象赋值时,SQLAlchemy会使用正确的ID值来创建外键的引用,SQL数据库用来完成关系的连接。

    Post.tags属性也被定义为关系。Tag由相关表被引用。SQLAlchemy会追踪ID值的变化,然后在SQL关系表中为我们创建必要的行。

    为了将PostBlog关联起来,将使用Blog.posts属性。同样地,在这里也被定义成了关系。当将Post对象列表赋值在这个关系属性上时,ORM会为每个Post对象创建正确的外键引用。这样做是有效的,因为在定义关系时,提供了backref属性。最后,通过如下代码提交这个会话。

    session.commit()

    数据库的插入操作都由自动生成的SQL来完成。对象保留在会话缓存中。如果应用继续使用这个会话的实例,这个对象池仍是可用的,而不必对数据库做任何实际的查询。

    另外,如果要完全确保在要提交的查询语句中包含了所有的并发更新的写操作,可以为那个查询创建一个新的空会话。当丢弃一个会话并使用空会话时,必须从数据库中重新取出对象来刷新会话。

    可以写一个简单的查询来检查并打印出所有的Blog对象。

    session= Session()
    for blog in session.query(Blog):
      print( "{title}\n{underline}\n".format(**blog.as_dict()) )
      for p in blog.entries:
        print( p.as_dict() )

    以上代码将获取所有的Blog实例。Blog.as_dict()方法将获取一个博客内的所有文章。Post.as_dict()将获取所有的标签。SQLAlchemy会自动生成SQL查询并执行。

    我们并没有包含其他格式,它们基于在第9章“序列化和保存——JSON、YAML、Pickle、CSV和XML”中的模板,这点并没有变化。我们可以从Blog对象通过entries列表导航到Post对象,而无需编写相关的SQL查询。将导航转换为查询时SQLAlchemy的职责。对于如何通过生成查询来刷新缓存并返回所期望的对象,使用一个Python迭代器就足够了。

    如果在Engine实例中有定义echo=True,那么我们就可以看到一组SQL查询的执行,它们会获取BlogPostTag实例。这些信息可以让我们了解应用对数据库服务器造成的工作负载。