写点什么

通过 demo 学习 OpenStack 开发——数据库 (1)

2016 年 1 月 26 日

编者按:《通过 demo 学习 OpenStack 开发》专栏是刘陈泓的系列文章,专栏通过开发一个 demo 的形式来介绍一些参与 OpenStack 项目开发的必要的基础知识,希望帮助大家入门企业级 Python 项目的开发和 OpenStack 项目的开发。刘陈泓主要关注 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 的选择

什么是 ORM

ORM 的全称是Object-Relational Mapping,即对象关系映射,是一种利用编程语言的对象来表示关系数据库中的数据的技术,其更形式化的定义可以参考 Wiki 页面 Orject-relational mapping 。简单的说,ORM 就是把数据库的一张表和编程语言中的一个对象对应起来,这样我们在编程语言中操作一个对象的时候,实际上就是在操作这张表,ORM(一般是一个库)负责把我们对一个对象的操作转换成对数据库的操作。

Python 中的 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

SQLAlchemy 简介

SQLAlchemy 项目是 Python 中最著名的 ORM 实现,不仅在 Python 项目中也得到了广泛的应用,而且对其他语言的 ORM 有很大的影响。OpenStack 一开始选择这个库,也是看中了它足够稳定、足够强大的特点。

SQLAlchemy 项目最新的版本是 1.0.11,1.0 系列是今年刚发的,0.9 系列应该还是应用最广泛的版本。对于一般的应用来说,0.9 系列和 1.0 系列差别不大。

关于 SQLAlchemy 的学习

我个人觉得 SQLAlchemy 的学习难度会比 Django 的 Model 层难一些,因为一个最简单的例子也会有一些不太直观的地方,对于没用过的人来说,会比较难以理解。不过 SQLAlchemy 官网整理了一些比较不错的入门教程,是一个比较好的学习起点: Tutorials 。另外,官方的 Reference 其实是一个很好的教程,讲了很多基本的概念,有助于理解 SQLAlchemy 的库的使用。 Reference 还提供 PDF 版本的下载。我个人建议大家直接阅读 Reference 即可,阅读顺序就按照 PDF 文件的章节编排顺序进行。虽然这个文档很长,但是我最后发现这么做是最节约时间的。

SQLAlchemy 的架构

先让我们来看一下 SQLAlchemy 这个库的总体架构,如下图(图来自官网)所示:

SQLAlchemy 这个库分为两层:

  • 上面这层是 ORM 层,为用户提供 ORM 接口,即通过操作 Python 对象来实现数据库操作的接口。

  • 下面这层是 Core 层,这层包含了 Schema/Types、SQL Expression Language、Engine 这三个部分:

    • SQL Expression Language 是 SQLAlchemy 中实现的一套 SQL 表达系统,主要是实现了对 SQL 的 DML(Data Manipulation Language) 的封装。这里实现了对数据库的 SELECT、DELETE、UPDATE 等语句的封装。SQL Expression Language 是实现 ORM 层的基础。
    • Schema/Types 这部分主要是实现了对 SQL 的 DDL(Data Definition Language) 的封装。实现了 Table 类用来表示一个表,Column 类用来表示一个列,也是实现了将数据库的数据类型映射到 Python 的数据类型。上面的 SQL Expression Language 的操作对象就是这里定义的 Table。
    • Engine 实现了对各种不同的数据库客户端的封装和调度,是所有 SQLAlchemy 应用程序的入口点,要使用 SQLAlchemy 库来操作一个数据库,首先就要有一个 Engine 对象,后续的所有对数据库的操作都要通过这个 Engine 对象来进行。下图是官方文档中的 Engine 位置的描述图:

    1. Pool 是 Engine 下面的一个模块,用来管理应用程序到数据库的连接。
    2. Dialect 是 Engine 下的另一个模块,用来对接不同的数据库驱动(即 DBMS 客户端),这些驱动要实现 DBAPI 接口。
  • 最后,SQLAlchemy 还要依赖各个数据库驱动的 DBAPI 接口来实现对数据库服务的调用。DBAPI 是 Python 定义的数据库 API 的实现规范,具体见 PEP0249

上面简单的总结了 SQLAlchemy 的架构,希望大家能够大概了解一下 SQLAlchemy,在后面介绍一些相关概念时,能够知道这个概念是属于整个架构的哪个部分。

Dialect 和数据库客户端

上面提到了 Dialect 是用来对接不同的数据库驱动的,它主要负责将 SQLAlchemy 最后生成的数据库操作转换成对数据库驱动的调用,其中会处理一些不同数据库和不同 DBAPI 实现的差别。这个部分一般是 SQLAlchemy 的开发者关心的内容,如果你只是使用 SQLAlchemy 来操作数据库,那么可以不用关心这个部分。不过我们还是要来了解一下 SQLAlchemy 支持的和 OpenStack 相关的数据库驱动。

MySQL

OpenStack 项目主要是使用 MySQL,之前一直都在使用 MySQL-Python驱动,因为这个驱动足够成熟和稳定。不过这个情况正在转变,有如下两个原因:

  1. MySQL-Python 不支持 Python3,而 OpenStack 正在转换到 Python3 的过程中,所以这个驱动最终是要放弃的。
  2. MySQL-Python 是用 C 语言写的,不支持 eventlet 库的 monkey-patch 操作,无法被 eventlet 库转换成异步操作,所以使用了 eventlet 库的到 OpenStack 项目在使用 MySQL 数据库时,都是进行同步的串行操作,有性能损失。

为了解决这个问题,社区发起了一次对新驱动的评估,主要是评估 MySQL-Python 驱动: PyMySQL Evaluation。这个评估还在社区的邮件列表发起了好几次讨论,到目前为止的结果是:如果使用 Python 2.7,那么继续使用 MySQL-Python 这个驱动,否则就使用 PyMySQL 这个驱动。PyMySQL 驱动是使用纯 Python 写的,不仅支持 Python3 而且可以支持 eventlet 的异步。

SQLite3

OpenStack 项目一般会使用 SQLite3 数据库来运行单元测试。OpenStack 在 Python2.7 下会使用 pysqlite 驱动,不过这个驱动和标准库中的 sqlite3 模块是一样的,也就是 Python 内置了 SQLite3 的驱动,你无需选择其他的驱动。

SQLAlchemy 的基本概念和使用

使用 SQLAlchemy 大体上分为三个步骤:连接到数据库,定义数据模型,执行数据操作。

连接到数据库

在你的应用可以使用数据库前,你要先定义好数据库的连接,包括数据库在哪里,用什么账号访问等。所有的这些工作都是通过 Engine 对象来进行的(记得上面提到的 Engine 了么?)。

数据库 URL

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

创建 Engine 对象

确定了要连接的数据库信息后,就可以通过create_engine函数来创建一个 Engine 对象了。

复制代码
from sqlalchemy import create_engine
engine = create_engine('sqlite://:memory:')

create_engine函数还支持以下几个参数:

  • connect_args:一个字典,用来自定义数据库连接的参数,比如指定客户端使用的字符编码。
  • pool_size 和 max_overflow:指定连接池的大小。
  • poolclass:指定连接池的实现
  • echo:一个布尔值,用来指定是否打印执行的 SQL 语句到日志中。

还有很多其他的参数,可以参考官方文档: Engine Configuration

一般来说,Engine 对象会默认启用连接池,会根据不同的 dialect 来选择不同的默认值。一般来说,你是不用考虑连接池的配置的,默认情况都配置好了。想了解关于连接池的更多内容,请查看官方文档: Connection Pooling

使用 Engine 对象

一般来说,应用程序的代码是不直接使用 Engine 对象的,而是把 Engine 对象交给 ORM 去使用,或者创建 session 对象来使用。不过,我们还是来简单看一下 Engine 对象能做什么事情。

应用程序可以调用 Engine 对象的connect()方法来获得一个到数据库的连接对象;然后可以在这个连接对象上调用execute()来执行 SQL 语句,调用begin()commit()rollback()来执行事务操作;调用close()来关闭连接。Engine 对象也有一些快捷方法来直接执行上述操作,避免了每次都要调用connect()来获取连接这种繁琐的代码,比如engine.execute()with engine.begin()等。

定义数据模型

有了数据库连接后,我们就可以来定义数据模型了,也就是定义映射数据库表的 Python 类。在 SQLAlchemy 中,这是通过Declarative的系统来完成的。

Declarative 系统

根据官方文档的描述,SQLAlchemy 一开始是采用下面这种方式来定义 ORM 的:

  1. 首先定义一个映射类,这个类是数据库表在代码中的对象表示,这类的类属性是很多 Column 类的实例。
  2. 然后定义一个 Table 对象,这里的 Table 就是上面提到的在Schema/Types模块中的一个类,用来表示一个数据库中的表。
  3. 调用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 的最主要的功能之一,不仅可以指定单表的映射,还能够指定表之间的关系。由于篇幅限制,我们在本文就不展开讲了。

Schema 和 Metadata

关于 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()
{1}
{1}
class Person(Base):
__tablename__ = 'person'
{1}
id = Column(Integer, primary_key=True)
name = Column(String(250), nullable=False)
{1}
{1}
engine = create_engine('sqlite:///:memory:', echo=True)
{1}
{1}
Base.metadata.create_all(engine)
{1}

输出结果如下:

复制代码
...
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

CRUD就是 CREATE、READ、UPDATE、DELETE,增删改查。这个也是 SQLAlchemy 中最常用的功能,而且都是通过上一小节中的session对象来使用的。我们这简单的介绍一下这四个操作,后面会给出官方文档的位置。

Create

在数据库中插入一条记录,是通过 session 的add()方法来实现的,你需要先创建一个映射类的实例,然后调用session.add()方法,然后调用session.commit()方法提交你的事务(关于事务,我们下面会专门讲解):

复制代码
new_person = Person(name='new person')
session.add(new_person)
session.commit()

Delete

删除操作和创建操作差不多,是把一个映射类实例传递给session.delete()方法。

Update

更新一条记录需要先使用查询操作获得一条记录对应的对象,然后修改对象的属性,再通过session.add()方法来完成更新操作。

Read

查询操作,一般称为 query,在 SQLAlchemy 中一般是通过Query 对象来完成的。我们可以通过 session.query() 方法来创建一个 Query 对象,然后调用 Query 对象的众多方法来完成查询操作。

事务

使用 session,就会涉及到事务,我们的应用程序也会有很多事务操作的要求。当你调用一个 session 的方法,导致 session 执行一条 SQL 语句时,它会自动开始一个事务,直到你下次调用session.commit()或者session.rollback(),它就会结束这个事务。你也可以显示的调用session.begin()来开始一个事务,并且session.begin()还可以配合 Python 的 with 来使用。

会话、CRUD、事务的小结

上面关于 session,CRUD 和事务的内容写的比较少,因为这些功能的内容很多,而且官方文档也写得很全面,本文就不做一些重复说明了。我们会在下一篇文章中通过 webdemo 的代码来看看如何使用这些功能。

总结

本文介绍了 OpenStack 中和数据库相关的一些知识,重点讲解了 SQLAlchemy 这个库的基本概念和架构。下一篇文章,我们会通过 demo 来实际项目中如何使用 SQLAlchemy。


感谢魏星对本文的策划和审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们,并与我们的编辑和其他读者朋友交流(欢迎加入 InfoQ 读者交流群(已满),InfoQ 读者交流群(#2))。

2016 年 1 月 26 日 16:143672

评论 1 条评论

发布
用户头像
写的很棒 正好学到这里
2020 年 11 月 22 日 00:16
回复
没有更多了
发现更多内容

物联网安全难题还需行业标杆来解

熵核科技

物联网安全

字节取消“大小周”,管理者与员工的“灵魂争夺战"从未停歇

Java 白

叱咤风云IT界!阿里首推的“SpringBoot+Vue全栈项目”到底有多牛X?

Java领路人

Java 编程 程序员 面试 架构师

架构实战营1期第二模块作业

五只羊

架构实战营

重温历史 致敬百年 “复兴大道100号”线上VR展馆正式开馆

百度大脑

百度 虚拟现实

7.24 杭州站 | 阿里云 Serverless Developer Meetup 开放报名!

Serverless Devs

云计算 阿里云 Serverless 云原生

神来之笔,2021CTF内核漏洞精选解析

网络安全学海

网络安全 信息安全 CTF 安全漏洞 渗透测试·

Vue进阶(幺叁捌):vue路由传参的几种基本方式

No Silver Bullet

Vue 路由 7月日更

熵核科技,自主研发虚拟机赋能安全操作系统

熵核科技

支付安全 安全操作系统 物联网安全 eSIM安全

医美行业哪个环节最赚钱?

石云升

行业分析 7月日更

再谈BOM和DOM(3):DOM节点操作-元素样式修改及DOM内容增删改查

zhoulujun

DOM BOM 文档对象 DOM结点操作 DOM增删改查

fil矿机怎么选择?用什么fil矿机比较好?

IPFS星盟小熊

FIL矿机怎么买 fil挖矿

登峰造极迎接实战!绝版Spring全家桶阿里高工私享面试进阶笔记,就这还不香?

Java领路人

Java 编程 程序员 架构师 架构 面试 Java

算法大赛报名 | OMG!这些名企的真实数据竟用来battle

工赋开发者社区

算法 工业互联网

攒塑料袋,究竟是如何刻进中国人DNA的?

脑极体

没想到我也可以入职阿里!二本毕业、两年crud经验,侥幸通过面试定级P6

Java架构师迁哥

三年开发经验,字节跳动抖音组离职后,一口气拿到15家公司Offer

Java架构师迁哥

再谈BOM和DOM(2):DOM节点层次/属性/选择器/节点关系/操作详解

zhoulujun

JavaScript DOM BOM 对象模型 文档模型

再谈BOM和DOM(4):DOM0/DOM2事件处理分析

zhoulujun

DOM DOM事件 DOM0 DOM2

数据仓库的基本概念

大数据技术指南

7月日更

性能测试软启动初探

FunTester

性能测试 接口测试 测试框架 压力测试 测试开发

再谈BOM和DOM(1):BOM与DOM概述

zhoulujun

JavaScript DOM BOM 对象模型 文档模型

保洁阿姨分享:腾讯架构师JDK源码笔记,13万字,带你飙向实战

Java架构师迁哥

5分钟速读之Rust权威指南(四十一)高级类型

码生笔谈

rust

架构实战营模块八作业

竹林七贤

再谈BOM和DOM(5):各个大流浪器DOM和BOM里面的那些坑—兼容性

zhoulujun

DOM事件兼容性

火爆 GitHub!这个图像分割神器开源了

百度大脑

百度 算法

JVM锁bug导致G1 GC挂起问题分析和解决

毕昇JDK社区

拥抱云原生,腾讯发布TCSS容器安全服务!

腾讯安全云鼎实验室

云原生 容器安全

Ta想做一粒智慧的种子

脑极体

抖音打击刷量控评行为:数据造假是互联网行业的毒瘤

石头IT视角

低代码的认知误区与落地实践

低代码的认知误区与落地实践

通过demo学习OpenStack开发——数据库(1)-InfoQ