编者按:《通过demo学习OpenStack开发》专栏是刘陈泓的系列文章,专栏通过开发一个demo的形式来介绍一些参与OpenStack项目开发的必要的基础知识,希望帮助大家入门企业级Python项目的开发和OpenStack项目的开发。刘陈泓主要关注OpenStack的身份认证和计费领域。另外,还对云计算、分布式系统应用和开发感兴趣。
OpenStack中的数据库应用主要是关系型数据库,主要使用的是MySQL数据库。当然也有一些NoSQL的应用,比如Ceilometer项目。就SQL数据库本身的应用而言,OpenStack的项目和其他项目并没有什么区别,也是采用ORM技术对数据进行增删改查而已。
本文的重点是讲解OpenStack项目中对关系型数据库的应用的基础知识,更多的是涉及ORM库的使用。对于数据库的安装和配置,需要读者自己查找一下MySQL的教程,如果只是为了验证ORM的相关知识,也可以使用sqlite数据库。
OpenStack官方推荐的保存生产数据的是MySQL数据库,在devstack项目(这个项目用于快速搭建OpenStack开发环境)中也是安装了MySQL数据库。不过,因为OpenStack的项目中没有使用特定的只有在MySQL上才能用的功能,而且所采用的ORM库SQLAlchemy也支持多种数据库,所以理论上选择PostgreSQL之类的数据库来替代MySQL也是可行的。
另外,OpenStack项目在单元测试中使用的是sqlite的内存数据库,这样开发者运行单元测试的时候不需要安装和配置复杂的MySQL数据库,只要安装好sqlite3就可以了。而且,数据库是保存在内存中的,会提高单元测试的速度。
ORM的全称是 Object-Relational Mapping ,即对象关系映射,是一种利用编程语言的对象来表示关系数据库中的数据的技术,其更形式化的定义可以参考 Wiki页面Orject-relational mapping 。简单的说,ORM就是把数据库的一张表和编程语言中的一个对象对应起来,这样我们在编程语言中操作一个对象的时候,实际上就是在操作这张表,ORM(一般是一个库)负责把我们对一个对象的操作转换成对数据库的操作。
一般来说,各种主流语言都有自己的ORM实现,一般来说也不只一种,比较出名的有Java的Hibernate,Ruby on Rails的ORM,C++的ODB等。 在Python中也存在多种ORM的实现 ,最著名的两种是 Django的Model层的ORM实现 ,以及 SQLAlchemy库 。这两种ORM实现基本上是Python中ORM的事实上的标准,如果你写Django应用,那么你就用Django自带的实现;不然,你就可以选择SQLAlchemy库。
OpenStack基本上都是Python项目,所以在OpenStack中,ORM主要是使用了SQLAlchemy库(Keystone、Nova、Neutron等);不过使用了Django的Horizon项目(面板)还是使用了Django自带的ORM实现。本文主要是讲解OpenStack中如何使用SQLAlchemy库,这个也是开发OpenStack项目的最基本知识。
SQLAlchemy项目是Python中最著名的ORM实现,不仅在Python项目中也得到了广泛的应用,而且对其他语言的ORM有很大的影响。OpenStack一开始选择这个库,也是看中了它足够稳定、足够强大的特点。
SQLAlchemy项目 最新的版本是1.0.11,1.0系列是今年刚发的,0.9系列应该还是应用最广泛的版本。对于一般的应用来说,0.9系列和1.0系列差别不大。
我个人觉得SQLAlchemy的学习难度会比Django的Model层难一些,因为一个最简单的例子也会有一些不太直观的地方,对于没用过的人来说,会比较难以理解。不过SQLAlchemy官网整理了一些比较不错的入门教程,是一个比较好的学习起点: Tutorials 。另外,官方的Reference其实是一个很好的教程,讲了很多基本的概念,有助于理解SQLAlchemy的库的使用。 Reference 还提供PDF版本的下载。我个人建议大家直接阅读Reference即可,阅读顺序就按照PDF文件的章节编排顺序进行。虽然这个文档很长,但是我最后发现这么做是最节约时间的。
先让我们来看一下SQLAlchemy这个库的总体架构,如下图(图来自官网)所示:
SQLAlchemy这个库分为两层:
Engine实现了对各种不同的数据库客户端的封装和调度,是所有SQLAlchemy应用程序的入口点,要使用SQLAlchemy库来操作一个数据库,首先就要有一个Engine对象,后续的所有对数据库的操作都要通过这个Engine对象来进行。下图是官方文档中的Engine位置的描述图:
上面简单的总结了SQLAlchemy的架构,希望大家能够大概了解一下SQLAlchemy,在后面介绍一些相关概念时,能够知道这个概念是属于整个架构的哪个部分。
上面提到了Dialect是用来对接不同的数据库驱动的,它主要负责将SQLAlchemy最后生成的数据库操作转换成对数据库驱动的调用,其中会处理一些不同数据库和不同DBAPI实现的差别。这个部分一般是SQLAlchemy的开发者关心的内容,如果你只是使用SQLAlchemy来操作数据库,那么可以不用关心这个部分。不过我们还是要来了解一下SQLAlchemy支持的和OpenStack相关的数据库驱动。
OpenStack项目主要是使用MySQL,之前一直都在使用 MySQL-Python 驱动,因为这个驱动足够成熟和稳定。不过这个情况正在转变,有如下两个原因:
为了解决这个问题,社区发起了一次对新驱动的评估,主要是评估 MySQL-Python 驱动: PyMySQL Evaluation 。这个评估还在社区的邮件列表发起了好几次讨论,到目前为止的结果是: 如果使用Python 2.7,那么继续使用MySQL-Python这个驱动 ,否则就使用PyMySQL这个驱动。PyMySQL驱动是使用纯Python写的,不仅支持Python3而且可以支持eventlet的异步。
OpenStack项目一般会使用SQLite3数据库来运行单元测试。OpenStack在Python2.7下会使用 pysqlite 驱动,不过这个驱动和标准库中的sqlite3模块是一样的,也就是Python内置了SQLite3的驱动,你无需选择其他的驱动。
SQLAlchemy的基本概念和使用
使用SQLAlchemy大体上分为三个步骤:连接到数据库,定义数据模型,执行数据操作。
在你的应用可以使用数据库前,你要先定义好数据库的连接,包括数据库在哪里,用什么账号访问等。所有的这些工作都是通过Engine对象来进行的(记得上面提到的Engine了么?)。
SQLAlchemy使用URL的方式来指定要访问的数据库,整个URL的具体格式如下:
dialect+driver://username:password@host:port/database
其中, dialect 就是指DBMS的名称,一般可选的值有: postgresql、 mysql、sqlite 等。 driver 就是指驱动的名称,如果不指定,SQLAlchemy会使用默认值。 database 就是指DBMS中的一个数据库,一般是指通过 CREATE DATABASE
语句创建的数据库。其他的参数就不言而喻了。dialect和driver参数有很多选择,具体的可以参考官方文档: Database URLs 。
确定了要连接的数据库信息后,就可以通过 create_engine
函数来创建一个Engine对象了。
from sqlalchemy import create_engine engine = create_engine('sqlite://:memory:')
create_engine
函数还支持以下几个参数:
还有很多其他的参数,可以参考官方文档: Engine Configuration 。
一般来说,Engine对象会默认启用连接池,会根据不同的dialect来选择不同的默认值。一般来说,你是不用考虑连接池的配置的,默认情况都配置好了。想了解关于连接池的更多内容,请查看官方文档: Connection Pooling 。
一般来说,应用程序的代码是不直接使用Engine对象的,而是把Engine对象交给ORM去使用,或者创建session对象来使用。不过,我们还是来简单看一下Engine对象能做什么事情。
应用程序可以调用Engine对象的 connect()
方法来获得一个到数据库的连接对象;然后可以在这个连接对象上调用 execute()
来执行SQL语句,调用 begin()
、 commit()
、 rollback()
来执行事务操作;调用 close()
来关闭连接。Engine对象也有一些快捷方法来直接执行上述操作,避免了每次都要调用 connect()
来获取连接这种繁琐的代码,比如 engine.execute()
、 with engine.begin()
等。
有了数据库连接后,我们就可以来定义数据模型了,也就是定义映射数据库表的Python类。在SQLAlchemy中,这是通过 Declarative 的系统来完成的。
根据官方文档的描述,SQLAlchemy一开始是采用下面这种方式来定义ORM的:
sqlalchemy.orm.mapper
函数把步骤1中定义的类映射到步骤2中定义的Table。 上面这种方式称为 Classical Mappings ,看起来好麻烦啊。所以就有了 Declarative 系统。这个系统就是一次完成这三个步骤,你只需要定义步骤1中的类即可。这也是现在在SQLAlchemy中使用ORM的方式,无需在使用过去这种麻烦的方法。
要使用Declarative系统,你需要为所有映射类创建一个基类,这个基类用来维护所有映射类的元信息。
from sqlalchemy.ext.declarative import declarative_base Base = declarative_base()
现在我们可以开始创建映射类了。假设我们在数据库中有一个表Person,这个表有两个列,分别是id和name,那么我们创建的映射类如下:
from sqlalchemy import Column, Integer, String # 这里的基类Base是上面我们通过declarative_base函数生成的 class Person(Base): __tablename__ = 'person' id = Column(Interger, primary_key=True) name = Column(String(250), nullable=False)
这样我们就定义了一个映射类 Person ,后续我们可以通过操作这个类的实例来实现对数据库表person的操作。在我们的映射类中,我们使用 __tablename__
属性来指定该映射类所对应的数据库表,通过 Column
类实例的方式来指定数据库的字段。这里,读者可能会问:我如何能知道 Column
都能支持哪些类型呢?这个查看官方文档获得: Column And Data Types 。
因为我们使用了Declarative系统,所以虽然我们自己没有定义Table对象,但是Declarative系统帮我们做了,并且帮我们调用了 mapper
函数。因此,当我们定义好一个表的映射类后,这个类的 __table__
属性就保存了该映射类所映射的 Table 对象:
In [6]: Person.__table__ Out[6]: Table('person', MetaData(bind=None), Column('id', Integer(), table=<person>, primary_key=True, nullable=False), Column('name', String(length=250), table=<person>, nullable=False), schema=None)
定义映射类是我们使用ORM的最主要的功能之一,不仅可以指定单表的映射,还能够指定表之间的关系。由于篇幅限制,我们在本文就不展开讲了。
关于Table对象,我们上面也提到了,它属于SQLAlchemy的core层的 Schema/Types 这个部分。SQLAlchemy中的Schema可以理解为和DDL相关的一套体系,它告诉SQLAlchemy的其他部分,数据库中的表是如何定义的。这个相当于我们在MySQL中使用 describe
命令,或者在PostgreSQL中使用 /d
命令。
SQLAlchemy中通过 schema metadata 来实现上面说的Schema。Schema metadata,官方文档中也称为database metadata,简称为metadata,是一个容器,其中包含了和DDL相关的所有信息,包括Table、Column等对象。当SQLAlchemy要根据映射类生成SQL语句时,它会查询metadata中的信息,根据信息来生成SQL语句。
为了要让metadata可以工作,我们需要把DDL的相关信息放到metadata中。如果你注意看上面 Person.__table__
的输出,就会发现 Table
类的第二个参数就是一个Metadata实例,也就是说,我们需要在定义Table的时候就把DDL信息放到metadata中。如果是是用classical mapping的方式,我们需要先创建一个metadata实例,然后每次创建一个Table对象的时候就把metadata传递进去。从写代码的角度来说,这个方式没有什么问题,也不算麻烦;问题是我们在使用ORM的过程中,几乎不会用到metadata,metadata基本上是给SQLAlchemy用的,对于用户来说metadata提供的接口只能用来创建表和删除表,这种操作的频率远低于查询操作。
好在Declarative系统则帮我们把这些都做好了。当我们通过 declarative_base()
生成一个基类Base的时候,这个基类就已经包含了一个metadata实例,后面基于Base定义映射类都会被自动加入到这个metadata中。我们可以通过 Base.metadata
来访问这个metadata实例。
说了这么多关于metadata的内容,简单总结一下:metadata是schema在SQLAlchemy中的实现,包含了DDL的信息,SQLAlchemy中的其他部分需要依赖于metadata中的信息,一般用户很少使用metadata。
很少用?那说这么多是做啥?主要是让读者可以理解下面这个语句的原理:
Base = declarative_base() # 基于Base定义映射类 Base.metadata.create_all(engine)
最后这行代码是我们最常用到metadata的地方:创建所有的表。我们告诉 create_all
使用哪个engine,它就会生成所有的 CREATE TABLE
语句,并且通过engine发送到数据库上执行。这个在单元测试的时候很有用。你可以执行一下下面的代码来观察输出:
from sqlalchemy import Column, Integer, String from sqlalchemy.ext.declarative import declarative_base from sqlalchemy import create_engine Base = declarative_base() class Person(Base): __tablename__ = 'person' id = Column(Integer, primary_key=True) name = Column(String(250), nullable=False) engine = create_engine('sqlite:///:memory:', echo=True) Base.metadata.create_all(engine)
输出结果如下:
... 2016-01-06 09:56:03,600 INFO sqlalchemy.engine.base.Engine PRAGMA table_info("person") 2016-01-06 09:56:03,601 INFO sqlalchemy.engine.base.Engine () 2016-01-06 09:56:03,602 INFO sqlalchemy.engine.base.Engine CREATE TABLE person ( id INTEGER NOT NULL, name VARCHAR(250) NOT NULL, PRIMARY KEY (id) ) 2016-01-06 09:56:03,603 INFO sqlalchemy.engine.base.Engine () 2016-01-06 09:56:03,603 INFO sqlalchemy.engine.base.Engine COMMIT
关于Metadata的更多信息,请查看官方文档: Schema Definition Language 。
会话( session )是我们通过SQLAlchemy来操作数据库的入口。我们前面有介绍过SQLAlchemy的架构,session是属于ORM层的。Session的功能是管理我们的程序和数据库之间的会话,它利用Engine的连接管理功能来实现会话。我们在上文有提到,我们创建了Engine对象,但是一般不直接使用它,而是把它交给ORM去使用。其中,通过session来使用Engine就是一个常用的方式。
要是用session,我们需要先通过 sessionmaker
函数创建一个session类,然后通过这个类的实例来使用会话,如下所示:
from sqlalchemy.orm import sessionmaker DBSession = sessionmaker(bind=engine) session = DBSession()
我们通过 sessionmaker
的 bind 参数把Engine对象传递给 DBSession
去管理。然后, DBSession
实例化的对象 session
就能被我们使用了。
CRUD就是CREATE、READ、UPDATE、DELETE,增删改查。这个也是SQLAlchemy中最常用的功能,而且都是通过上一小节中的 session
对象来使用的。我们这简单的介绍一下这四个操作,后面会给出官方文档的位置。
在数据库中插入一条记录,是通过session的 add()
方法来实现的,你需要先创建一个映射类的实例,然后调用 session.add()
方法,然后调用 session.commit()
方法提交你的事务(关于事务,我们下面会专门讲解):
new_person = Person(name='new person') session.add(new_person) session.commit()
删除操作和创建操作差不多,是把一个映射类实例传递给 session.delete()
方法。
更新一条记录需要先使用查询操作获得一条记录对应的对象,然后修改对象的属性,再通过 session.add()
方法来完成更新操作。
查询操作,一般称为query,在SQLAlchemy中一般是通过 Query对象 来完成的。我们可以通过session.query()方法来创建一个Query对象,然后调用Query对象的众多方法来完成查询操作。
使用session,就会涉及到事务,我们的应用程序也会有很多事务操作的要求。当你调用一个session的方法,导致session执行一条SQL语句时,它会自动开始一个事务,直到你下次调用 session.commit()
或者 session.rollback()
,它就会结束这个事务。你也可以显示的调用 session.begin()
来开始一个事务,并且 session.begin()
还可以配合Python的with来使用。
上面关于session,CRUD和事务的内容写的比较少,因为这些功能的内容很多,而且官方文档也写得很全面,本文就不做一些重复说明了。我们会在下一篇文章中通过webdemo的代码来看看如何使用这些功能。
本文介绍了OpenStack中和数据库相关的一些知识,重点讲解了SQLAlchemy这个库的基本概念和架构。下一篇文章,我们会通过demo来实际项目中如何使用SQLAlchemy。
感谢魏星对本文的策划和审校。
给InfoQ中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家通过新浪微博(@InfoQ,@丁晓昀),微信(微信号: InfoQChina )关注我们,并与我们的编辑和其他读者朋友交流(欢迎加入InfoQ读者交流群 (已满),InfoQ读者交流群(#2) )。