Python开发企业级项目和OpenStack项目

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

《通 过demo学习OpenStack开发》专栏是刘陈泓的系列文章,专栏通过开发一个demo的形式来介绍一些参与OpenStack项目开发的必要的基础 知识,希望帮助大家入门企业级Python项目的开发和OpenStack项目的开发。刘陈泓主要关注OpenStack的身份认证和计费领域。另外,还 对云计算、分布式系统应用和开发感兴趣。

在上一篇文章,我们介绍了SQLAlchemy的基本概念,也介绍了基本的使用流程。本文我们结合webdemo这个项目来介绍如何在项目中使用SQLAlchemy。另外,我们还会介绍数据库版本管理的概念和实践,这也是OpenStack每个项目都需要做的事情。

Webdemo中的数据模型的定义和实现

我们之前在webdemo项目中已经开发了一个user管理的API,可以在这里回顾。当时只是接收了API请求并且打印信息,并没有实际的进行数据存储。现在我们就要引入数据库操作,来完成user管理的API。

User数据模型

在开发数据库应用前,需要先定义好数据模型。因为本文只是要演示SQL Alchemy的应用,所以我们定义个最简单的数据模型。user表的定义如下:

  • id: 主键,一般由数据库的自增类型实现。
  • user_id: user id,是一个UUID字符串,是OpenStack中最常用来标记资源的方式,全局唯一,并且为该字段建立索引。
  • name: user的名称,允许修改,全局唯一,不能为空。
  • email: user的email,允许修改,可以为空。

搭建数据库层的代码框架

OpenStack项目中我见过两种数据库的代码框架分隔,一种是Keystone的风格,它把一组API的API代码和数据库代码都放在同一个目录下,如下所示:

 

采用Pecan框架的项目则大多把数据库相关代码都放在db目录下,比如Magnum项目,如下所示:

由于webdemo采用的是Pecan框架,而且把数据库操作的代码放到同一个目录下也会比较清晰,所以我们采用和Magnum项目相同的方式来编写数据库相关的代码,创建webdemo/db目录,然后把数据库操作的相关代码都放在这个目录下,如下所示:

由于webdemo项目还没有使用oslo_db库,所以代码看起来比较直观,没有Magnum项目复杂。接下来,我们就要开始写数据库操作的相关代码,分为两个步骤:

1. 在**db/models.py**中定义`User`类,对应数据库的user表。
2. 在**db/api.py**中实现一个`Connection`类,这个类封装了所有的数据库操作接口。我们会在这个类中实现对user表的CRUD等操作。 

定义User数据模型映射类

db/models.py中的代码如下:

 1 from sqlalchemy import Column, Integer, String
 2 from sqlalchemy.ext import declarative
 3 from sqlalchemy import Index
 4 
 5 Base = declarative.declarative_base()
 6 
 7 class User(Base):
 8     """User table"""
 9 
10     __tablename__ = 'user'
11     __table_args__ = (
12         Index('ix_user_user_id', 'user_id'),
13     )
14     id = Column(Integer, primary_key=True)
15     user_id = Column(String(255), nullable=False)
16     name = Column(String(64), nullable=False, unique=True)
17     email = Column(String(255))

我们按照我们之前定义的数据模型,实现了映射类。

实现DB API

DB通用函数

在db/api.py中,我们先定义了一些通用函数,代码如下:

 1 from sqlalchemy import create_engine
 2 import sqlalchemy.orm
 3 from sqlalchemy.orm import exc
 4 
 5 from webdemo.db import models as db_models
 6 
 7 _ENGINE = None
 8 _SESSION_MAKER = None
 9 
10 def get_engine():
11     global _ENGINE
12     if _ENGINE is not None:
13         return _ENGINE
14 
15     _ENGINE = create_engine('sqlite://')
16     db_models.Base.metadata.create_all(_ENGINE)
17     return _ENGINE
18 
19 def get_session_maker(engine):
20     global _SESSION_MAKER
21     if _SESSION_MAKER is not None:
22         return _SESSION_MAKER
23 
24     _SESSION_MAKER = sqlalchemy.orm.sessionmaker(bind=engine)
25     return _SESSION_MAKER
26 
27 def get_session():
28     engine = get_engine()
29     maker = get_session_maker(engine)
30     session = maker()
31 
32     return session

上面的代码中,我们定义了三个函数:

  • get_engine:返回全局唯一的engine,不需要重复分配。

  • get_session_maker:返回全局唯一的session maker,不需要重复分配。

  • get_session:每次返回一个新的session,因为一个session不能同时被两个数据库客户端使用。

这三个函数是使用SQL Alchemy中经常会封装的,所以OpenStack的oslo_db项目就封装了这些函数,供所有的OpenStack项目使用。

这里需要注意一个地方,在get_engine()中:

1  _ENGINE = create_engine('sqlite://')
2     db_models.Base.metadata.create_all(_ENGINE)

我们使用了sqlite内存数据库,并且立刻创建了所有的表。这么做只是为了演示方便。在实际的项目中,create_engine()的数据库URL参数应该是从配置文件中读取的,而且也不能在创建engine后就创建所有的表(这样数据库的数据都丢了)。要解决在数据库中建表的问题,就要先了解数据库版本管理的知识,也就是database migration,我们在下文中会说明。

Connection实现

Connection的实现就简单得多了,直接看代码。这里只实现了get_user()和list_users()方法。

 1 class Connection(object):
 2 
 3     def __init__(self):
 4         pass
 5 
 6     def get_user(self, user_id):
 7         query = get_session().query(db_models.User).filter_by(user_id=user_id)
 8         try:
 9             user = query.one()
10         except exc.NoResultFound:
11             # TODO(developer): process this situation
12             pass
13 
14         return user
15 
16     def list_users(self):
17         session = get_session()
18         query = session.query(db_models.User)
19         users = query.all()
20 
21         return users
22 
23     def update_user(self, user):
24         pass
25 
26     def delete_user(self, user):
27         pass

在API Controller中使用DB API

现在我们有了DB API,接下来就是要在Controller中使用它。对于使用Pecan框架的应用来说,我们定义一个Pecan hook,这个hook在每个请求进来的时候实例化一个db的Connection对象,然后在controller代码中我们可以直接使用这个Connection实例。关于Pecan hook的相关信息,请查看Pecan官方文档

首先,我们要实现这个hook,并且加入到app中。hook的实现代码在webdemo/api/hooks.py中:

1 from pecan import hooks
2 
3 from webdemo.db import api as db_api
4 
5 class DBHook(hooks.PecanHook):
6     """Create a db connection instance."""
7 
8     def before(self, state):
9         state.request.db_conn = db_api.Connection()

然后,修改webdemo/api/app.py中的setup_app()方法:

 1 def setup_app():
 2     config = get_pecan_config()
 3 
 4     app_hooks = [hooks.DBHook()]
 5     app_conf = dict(config.app)
 6     app = pecan.make_app(
 7         app_conf.pop('root'),
 8         logging=getattr(config, 'logging', {}),
 9         hooks=app_hooks,
10         **app_conf
11     )
12 
13     return app

现在,我们就可以在controller使用DB API了。我们这里要重新实现API服务(4)实现的GET /v1/users这个接口:

 1 ...
 2 class User(wtypes.Base):
 3     id = int
 4     user_id = wtypes.text
 5     name = wtypes.text
 6     email = wtypes.text
 7 
 8 
 9 class Users(wtypes.Base):
10     users = [User]
11 ...
12 class UsersController(rest.RestController):
13 
14     @pecan.expose()
15     def _lookup(self, user_id, *remainder):
16         return UserController(user_id), remainder
17 
18     @expose.expose(Users)
19     def get(self):
20         db_conn = request.db_conn     # 获取DBHook中创建的Connection实例
21         users = db_conn.list_users()  # 调用所需的DB API
22         users_list = []
23         for user in users:
24             u = User()
25             u.id = user.id
26             u.user_id = user.user_id
27             u.name = user.name
28             u.email = user.email
29             users_list.append(u)
30         return Users(users=users_list)
31 
32     @expose.expose(None, body=User, status_code=201)
33     def post(self, user):
34         print user

现在,我们就已经完整的实现了这个API,客户端访问API时是从数据库拿数据,而不是返回一个模拟的数据。读者可以使用API服务(4)中的方法运行测试服务器来测试这个API。注意:由于数据库操作依赖于SQL Alchemy库,所以需要把它添加到requirement.txt中:SQLAlchemy<1.1.0,>=0.9.9。

小结

现在我们已经完成了数据库层的代码框架搭建,读者可以大概了解到一个OpenStack项目中是如何进行数据库操作的。上面的代码可以到GitHub下载。

数据库版本管理

数据库版本管理的概念

上面我们在get_engine()函数中使用了内存数据库,并且创建了所有的表。在实际项目中,这么做肯定是不行的:

1.  实际项目中不会使用内存数据库,这种数据库一般只是在单元测试中使用。
2.  如果每次`create_engine`都把数据库的表重新创建一次,那么数据库中的数据就丢失了,绝对不可容忍。 

解决这个问题的办法也很简单:不使用内存数据库,并且在运行项目代码前先把数据库中的表都建好。这么做确实是解决了问题,但是看起来有点麻烦:

  1. 如果每次都手动写SQL语句来创建数据库中的表,会很容易出错,而且很麻烦。
  2. 如果项目修改了数据模型,那么不能简单的修改建表的SQL语句,因为重新建表会让数据丢失。我们只能增加新的SQL语句来修改现有的数据库。
  3. 最关键的是:我们怎么知道一个正在生产运行的数据库是要执行那些SQL语句?如果数据库第一次使用,那么执行全部的语句是正确的;如果数据库已经在使用,里面有数据,那么我们只能执行那些修改表定义的SQL语句,而不能执行那些重新建表的SQL语句。

为了解决这种问题,就有人发明了数据库版本管理的概念,也称为Database Migration。基本原理是:在我们要使用的数据库中建立一张表,里面保存了数据库的当前版本,然后我们在代码中为每个数据库版本写好所需的SQL语句。当对一个数据库执行migration操作时,会执行从当前版本到目标版本之间的所有SQL语句。举个例子:

1.  在Version 1时,我们在数据库中建立一个user表。
2.  在Version 2时,我们在数据库中建立一个project表。
3.  在Version 3时,我们修改user表,增加一个age列。 

那么在我们对一个数据库执行migration操作,数据库的当前版本Version 1,我们设定的目标版本是Version 3,那么操作就是:建立一个project表,修改user表,增加一个age列,并且把数据库当前版本设置为Version 3。

数据库的版本管理是所有大型数据库项目的需求,每种语言都有自己的解决方案。OpenStack中主要使用SQL Alchemy的两种解决方案:sqlalchemy-migrateAlembic。 早期的OpenStack项目使用了sqlalchemy-migrate,后来换成了Alembic。做出这个切换的主要原因是Alembic对数据库 版本的设计和管理更灵活,可以支持分支,而sqlalchemy-migrate只能支持直线的版本管理,具体可以看OpenStack的WiKi文档Alembic

接下来,我们就在我们的webdemo项目中引入Alembic来进行版本管理。

Alembic

要使用Alembic,大概需要以下步骤:

1.  安装Alembic
2.  在项目中创建Alembic的migration环境
3.  修改Alembic配置文件
4.  创建migration脚本
5.  执行迁移动作 

看起来步骤很复杂,其实搭建好环境后,新增数据库版本只需要执行最后两个步骤。

安装Alembic

在webdemo/requirements.txt中加入:alembic>=0.8.0。然后在virtualenv中安装即可。

在项目中创建Alembic的migration环境

一般OpenStack项目中,Alembic的环境都是放在db/sqlalchemy/目录下,因此,我们先建立目录webdemo/db/sqlalchemy/,然后在这个目录下初始化Alembic环境:

(.venv)? ~/programming/python/webdemo git:(master) ? $ cd webdemo/db
(.venv)? ~/programming/python/webdemo/webdemo/db git:(master) ? $ ls
api.py  api.pyc  __init__.py  __init__.pyc  models.py  models.pyc  sqlalchemy
(.venv)? ~/programming/python/webdemo/webdemo/db git:(master) ? $ cd sqlalchemy
(.venv)? ~/programming/python/webdemo/webdemo/db/sqlalchemy git:(master) ? $ ls
(.venv)? ~/programming/python/webdemo/webdemo/db/sqlalchemy git:(master) ? $ alembic init alembic
  Creating directory /home/diabloneo/programming/python/webdemo/webdemo/db/sqlalchemy/alembic ... done
  Creating directory /home/diabloneo/programming/python/webdemo/webdemo/db/sqlalchemy/alembic/versions ... done
  Generating /home/diabloneo/programming/python/webdemo/webdemo/db/sqlalchemy/alembic/script.py.mako ... done
  Generating /home/diabloneo/programming/python/webdemo/webdemo/db/sqlalchemy/alembic.ini ... done
  Generating /home/diabloneo/programming/python/webdemo/webdemo/db/sqlalchemy/alembic/README ... done
  Generating /home/diabloneo/programming/python/webdemo/webdemo/db/sqlalchemy/alembic/env.pyc ... done
  Generating /home/diabloneo/programming/python/webdemo/webdemo/db/sqlalchemy/alembic/env.py ... done
  Please edit configuration/connection/logging settings in '/home/diabloneo/programming/python/webdemo/webdemo/db/sqlalchemy/alembic.ini' before proceeding.
(.venv)? ~/programming/python/webdemo/webdemo/db/sqlalchemy git:(master) ? $

现在,我们就在webdemo/db/sqlalchemy/alembic/目录下建立了一个Alembic migration环境:

修改Alembic配置文件

webdemo/db/sqlalchemy/alembic.ini文件是Alembic的配置文件,我们现在需要修改文件中的sqlalchemy.url这个配置项,用来指向我们的数据库。这里,我们使用SQLite数据库,数据库文件存放在webdemo项目的根目录下,名称是webdemo.db:

1 # sqlalchemy.url = driver://user:pass@localhost/dbname
2 sqlalchemy.url = sqlite:///../../../webdemo.db

注意:实际项目中,数据库的URL信息是从项目配置文件中读取,然后通过动态的方式传递给Alembic的。具体的做法,读者可以参考Magnum项目的实现

创建migration脚本

现在,我们可以创建第一个迁移脚本了,我们的第一个数据库版本就是创建我们的user表:

1 (.venv)? ~/programming/python/webdemo/webdemo/db/sqlalchemy git:(master) ? $ alembic revision -m "Create user table"
2   Generating /home/diabloneo/programming/python/webdemo/webdemo/db/sqlalchemy/alembic/versions/4bafdb464737_create_user_table.py ... done

现在脚本已经帮我们生成好了,不过这个只是一个空的脚本,我们需要自己实现里面的具体操作,补充完整后的脚本如下:

 1 """Create user table
 2 
 3 Revision ID: 4bafdb464737
 4 Revises:
 5 Create Date: 2016-02-21 12:24:46.640894
 6 
 7 """
 8 
 9 # revision identifiers, used by Alembic.
10 revision = '4bafdb464737'
11 down_revision = None
12 branch_labels = None
13 depends_on = None
14 
15 from alembic import op
16 import sqlalchemy as sa
17 
18 def upgrade():
19     op.create_table(
20         'user',
21         sa.Column('id', sa.Integer, primary_key=True),
22         sa.Column('user_id', sa.String(255), nullable=False),
23         sa.Column('name', sa.String(64), nullable=False, unique=True),
24         sa.Column('email', sa.String(255))
25     )
26 
27 def downgrade():
28     op.drop_table('user')

其实就是把User类的定义再写了一遍,使用了Alembic提供的接口来方便的创建和删除表。

执行迁移操作

我们需要在webdemo/db/sqlalchemy/目录下执行迁移操作,可能需要手动指定PYTHONPATH:

1 (.venv)? ~/programming/python/webdemo/webdemo/db/sqlalchemy git:(master) ? $ PYTHONPATH=../../../ alembic upgrade head
2 INFO  [alembic.migration] Context impl SQLiteImpl.
3 INFO  [alembic.migration] Will assume non-transactional DDL.
4 INFO  [alembic.migration] Running upgrade  -> 4bafdb464737, Create user table

alembic upgrade head会把数据库升级到最新的版本。这个时候,在webdemo的根目录下会出现webdemo.db这个文件,可以使用sqlite3命令查看内容:

 1 (.venv)? ~/programming/python/webdemo git:(master) ? $ ls
 2 AUTHORS  build  ChangeLog  dist  LICENSE  README.md  requirements.txt  Session.vim  setup.cfg  setup.py  webdemo  webdemo.db  webdemo.egg-info
 3 (.venv)? ~/programming/python/webdemo git:(master) ? $ sqlite3 webdemo.db
 4 SQLite version 3.8.11.1 2015-07-29 20:00:57
 5 Enter ".help" for usage hints.
 6 sqlite> .tables
 7 alembic_version  user
 8 sqlite> .schema alembic_version
 9 CREATE TABLE alembic_version (
10         version_num VARCHAR(32) NOT NULL
11 );
12 sqlite> .schema user
13 CREATE TABLE user (
14         id INTEGER NOT NULL,
15         user_id VARCHAR(255) NOT NULL,
16         name VARCHAR(64) NOT NULL,
17         email VARCHAR(255),
18         PRIMARY KEY (id),
19         UNIQUE (name)
20 );
21 sqlite> .header on
22 sqlite> select * from alembic_version;
23 version_num
24 4bafdb464737

测试新的数据库

现在我们可以把之前使用的内存数据库换掉,使用我们的文件数据库,修改get_engine()函数:

1 def get_engine():
2     global _ENGINE
3     if _ENGINE is not None:
4         return _ENGINE
5 
6     _ENGINE = create_engine('sqlite:///webdemo.db')
7     return _ENGINE

现在你可以手动往webdemo.db中添加数据,然后测试下API:

 1 ? ~/programming/python/webdemo git:(master) ? $ sqlite3 webdemo.db
 2 SQLite version 3.8.11.1 2015-07-29 20:00:57
 3 Enter ".help" for usage hints.
 4 sqlite> .header on
 5 sqlite> select * from user;
 6 sqlite> .schema user
 7 CREATE TABLE user (
 8         id INTEGER NOT NULL,
 9         user_id VARCHAR(255) NOT NULL,
10         name VARCHAR(64) NOT NULL,
11         email VARCHAR(255),
12         PRIMARY KEY (id),
13         UNIQUE (name)
14 );
15 sqlite> insert into user values(1, "user_id", "Alice", "[email protected]");
16 sqlite> select * from user;
17 id|user_id|name|email
18 1|user_id|Alice|[email protected]
19 sqlite> .q
20 ? ~/programming/python/webdemo git:(master) ? $
21 ? ~/programming/python/webdemo git:(master) ? $ curl http://localhost:8080/v1/users
22 {"users": [{"email": "[email protected]", "user_id": "user_id", "id": 1, "name": "Alice"}]}%

现 在,我们就已经完成了database migration代码框架的搭建,可以成功执行了第一个版本的数据库迁移。OpenStack项目中也是这么来做数据库迁移的。后续,一旦修改了项目, 需要修改数据模型时,只要新增migration脚本即可。这部分代码也可以在GitHub中看到。

在实际生产环境中,当我们发布了一个项目的新版本后,在上线的时候,都会自动执行数据库迁移操作,升级数据库版本到最新的版本。如果线上的数据库版本已经是最新的,那么这个操作没有任何影响;如果不是最新的,那么会把数据库升级到最新的版本。

关于Alembic的更多使用方法,请阅读官方文档Alembic

总结

本文到这边就结束了,这两篇文章我们了解OpenStack中数据库应用开发的基础知识。接下来,我们将会了解OpenStack中单元测试的相关知识。

猜你喜欢

转载自www.cnblogs.com/huohuohuo1/p/9175207.html
今日推荐