在上一篇文章,咱們介紹了SQLAlchemy的基本概念,也介紹了基本的使用流程。本文咱們結合webdemo這個項目來介紹如何在項目中使用SQLAlchemy。另外,咱們還會介紹數據庫版本管理的概念和實踐,這也是OpenStack每一個項目都須要作的事情。html
咱們以前在webdemo項目中已經開發了一個user管理的API,能夠在這裏回顧。當時只是接收了API請求而且打印信息,並無實際的進行數據存儲。如今咱們就要引入數據庫操做,來完成user管理的API。python
在開發數據庫應用前,須要先定義好數據模型。由於本文只是要演示SQLAlchemy的應用,因此咱們定義個最簡單的數據模型。user表的定義以下:git
id: 主鍵,通常由數據庫的自增類型實現。github
user_id: user id,是一個UUID字符串,是OpenStack中最經常使用來標記資源的方式,全局惟一,而且爲該字段創建索引。web
name: user的名稱,容許修改,全局惟一,不能爲空。sql
email: user的email,容許修改,能夠爲空。數據庫
OpenStack項目中我見過兩種數據庫的代碼框架分隔,一種是Keystone的風格,它把一組API的API代碼和數據庫代碼都放在同一個目錄下,以下所示:vim
採用Pecan框架的項目則大多把數據庫相關代碼都放在db目錄下,好比Magnum項目,以下所示:segmentfault
因爲webdemo採用的是Pecan框架,並且把數據庫操做的代碼放到同一個目錄下也會比較清晰,因此咱們採用和Magnum項目相同的方式來編寫數據庫相關的代碼,建立webdemo/db目錄,而後把數據庫操做的相關代碼都放在這個目錄下,以下所示:api
因爲webdemo項目尚未使用oslo_db庫,因此代碼看起來比較直觀,沒有Magnum項目複雜。接下來,咱們就要開始寫數據庫操做的相關代碼,分爲兩個步驟:
在db/models.py中定義User
類,對應數據庫的user表。
在db/api.py中實現一個Connection
類,這個類封裝了全部的數據庫操做接口。咱們會在這個類中實現對user表的CRUD等操做。
db/models.py中的代碼以下:
from sqlalchemy import Column, Integer, String from sqlalchemy.ext import declarative from sqlalchemy import Index Base = declarative.declarative_base() class User(Base): """User table""" __tablename__ = 'user' __table_args__ = ( Index('ix_user_user_id', 'user_id'), ) id = Column(Integer, primary_key=True) user_id = Column(String(255), nullable=False) name = Column(String(64), nullable=False, unique=True) email = Column(String(255))
咱們按照咱們以前定義的數據模型,實現了映射類。
在db/api.py中,咱們先定義了一些通用函數,代碼以下:
from sqlalchemy import create_engine import sqlalchemy.orm from sqlalchemy.orm import exc from webdemo.db import models as db_models _ENGINE = None _SESSION_MAKER = None def get_engine(): global _ENGINE if _ENGINE is not None: return _ENGINE _ENGINE = create_engine('sqlite://') db_models.Base.metadata.create_all(_ENGINE) return _ENGINE def get_session_maker(engine): global _SESSION_MAKER if _SESSION_MAKER is not None: return _SESSION_MAKER _SESSION_MAKER = sqlalchemy.orm.sessionmaker(bind=engine) return _SESSION_MAKER def get_session(): engine = get_engine() maker = get_session_maker(engine) session = maker() return session
上面的代碼中,咱們定義了三個函數:
get_engine
:返回全局惟一的engine,不須要重複分配。
get_session_maker
:返回全局惟一的session maker,不須要重複分配。
get_session
:每次返回一個新的session,由於一個session不能同時被兩個數據庫客戶端使用。
這三個函數是使用SQLAlchemy中常常會封裝的,因此OpenStack的oslo_db項目就封裝了這些函數,供全部的OpenStack項目使用。
這裏須要注意一個地方,在get_engine()
中:
_ENGINE = create_engine('sqlite://') db_models.Base.metadata.create_all(_ENGINE)
咱們使用了sqlite內存數據庫,而且馬上建立了全部的表。這麼作只是爲了演示方便。在實際的項目中,create_engine()
的數據庫URL參數應該是從配置文件中讀取的,並且也不能在建立engine後就建立全部的表(這樣數據庫的數據都丟了)。要解決在數據庫中建表的問題,就要先了解數據庫版本管理的知識,也就是database migration,咱們在下文中會說明。
Connection
的實現就簡單得多了,直接看代碼。這裏只實現了get_user()
和list_users()
方法。
class Connection(object): def __init__(self): pass def get_user(self, user_id): query = get_session().query(db_models.User).filter_by(user_id=user_id) try: user = query.one() except exc.NoResultFound: # TODO(developer): process this situation pass return user def list_users(self): session = get_session() query = session.query(db_models.User) users = query.all() return users def update_user(self, user): pass def delete_user(self, user): pass
如今咱們有了DB API,接下來就是要在Controller中使用它。對於使用Pecan框架的應用來講,咱們定義一個Pecan hook,這個hook在每一個請求進來的時候實例化一個db的Connection
對象,而後在controller代碼中咱們能夠直接使用這個Connection
實例。關於Pecan hook的相關信息,請查看Pecan官方文檔。
首先,咱們要實現這個hook,而且加入到app中。hook的實現代碼在webdemo/api/hooks.py中:
from pecan import hooks from webdemo.db import api as db_api class DBHook(hooks.PecanHook): """Create a db connection instance.""" def before(self, state): state.request.db_conn = db_api.Connection()
而後,修改webdemo/api/app.py中的setup_app()
方法:
def setup_app(): config = get_pecan_config() app_hooks = [hooks.DBHook()] app_conf = dict(config.app) app = pecan.make_app( app_conf.pop('root'), logging=getattr(config, 'logging', {}), hooks=app_hooks, **app_conf ) return app
如今,咱們就能夠在controller使用DB API了。咱們這裏要從新實現API服務(4)實現的GET /v1/users這個接口:
... class User(wtypes.Base): id = int user_id = wtypes.text name = wtypes.text email = wtypes.text class Users(wtypes.Base): users = [User] ... class UsersController(rest.RestController): @pecan.expose() def _lookup(self, user_id, *remainder): return UserController(user_id), remainder @expose.expose(Users) def get(self): db_conn = request.db_conn # 獲取DBHook中建立的Connection實例 users = db_conn.list_users() # 調用所需的DB API users_list = [] for user in users: u = User() u.id = user.id u.user_id = user.user_id u.name = user.name u.email = user.email users_list.append(u) return Users(users=users_list) @expose.expose(None, body=User, status_code=201) def post(self, user): print user
如今,咱們就已經完整的實現了這個API,客戶端訪問API時是從數據庫拿數據,而不是返回一個模擬的數據。讀者可使用API服務(4)中的方法運行測試服務器來測試這個API。注意:因爲數據庫操做依賴於SQLAlchemy庫,因此須要把它添加到requirement.txt中:SQLAlchemy<1.1.0,>=0.9.9。
如今咱們已經完成了數據庫層的代碼框架搭建,讀者能夠大概瞭解到一個OpenStack項目中是如何進行數據庫操做的。上面的代碼能夠到https://github.com/diabloneo/webdemo下載。
上面咱們在get_engine()
函數中使用了內存數據庫,而且建立了全部的表。在實際項目中,這麼作確定是不行的:
實際項目中不會使用內存數據庫,這種數據庫通常只是在單元測試中使用。
若是每次create_engine
都把數據庫的表從新建立一次,那麼數據庫中的數據就丟失了,絕對不可容忍。
解決這個問題的辦法也很簡單:不使用內存數據庫,而且在運行項目代碼前先把數據庫中的表都建好。這麼作確實是解決了問題,可是看起來有點麻煩:
若是每次都手動寫SQL語句來建立數據庫中的表,會很容易出錯,並且很麻煩。
若是項目修改了數據模型,那麼不能簡單的修改建表的SQL語句,由於從新建表會讓數據丟失。咱們只能增長新的SQL語句來修改現有的數據庫。
最關鍵的是:咱們怎麼知道一個正在生產運行的數據庫是要執行那些SQL語句?若是數據庫第一次使用,那麼執行所有的語句是正確的;若是數據庫已經在使用,裏面有數據,那麼咱們只能執行那些修改表定義的SQL語句,而不能執行那些從新建表的SQL語句。
爲了解決這種問題,就有人發明了數據庫版本管理的概念,也稱爲Database Migration。基本原理是:在咱們要使用的數據庫中創建一張表,裏面保存了數據庫的當前版本,而後咱們在代碼中爲每一個數據庫版本寫好所需的SQL語句。當對一個數據庫執行migration操做時,會執行從當前版本到目標版本之間的全部SQL語句。舉個例子:
在Version 1時,咱們在數據庫中創建一個user表。
在Version 2時,咱們在數據庫中創建一個project表。
在Version 3時,咱們修改user表,增長一個age列。
那麼在咱們對一個數據庫執行migration操做,數據庫的當前版本Version 1,咱們設定的目標版本是Version 3,那麼操做就是:創建一個project表,修改user表,增長一個age列,而且把數據庫當前版本設置爲Version 3。
數據庫的版本管理是全部大型數據庫項目的需求,每種語言都有本身的解決方案。OpenStack中主要使用SQLAlchemy的兩種解決方案:sqlalchemy-migrate和Alembic。早期的OpenStack項目使用了sqlalchemy-migrate,後來換成了Alembic。作出這個切換的主要緣由是Alembic對數據庫版本的設計和管理更靈活,能夠支持分支,而sqlalchemy-migrate只能支持直線的版本管理,具體能夠看OpenStack的WiKi文檔Alembic。
接下來,咱們就在咱們的webdemo項目中引入Alembic來進行版本管理。
要使用Alembic,大概須要如下步驟:
安裝Alembic
在項目中建立Alembic的migration環境
修改Alembic配置文件
建立migration腳本
執行遷移動做
看起來步驟很複雜,其實搭建好環境後,新增數據庫版本只須要執行最後兩個步驟。
在webdemo/requirements.txt中加入:alembic>=0.8.0。而後在virtualenv中安裝便可。
通常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環境:
webdemo/db/sqlalchemy/alembic.ini文件是Alembic的配置文件,咱們如今須要修改文件中的sqlalchemy.url這個配置項,用來指向咱們的數據庫。這裏,咱們使用SQLite數據庫,數據庫文件存放在webdemo項目的根目錄下,名稱是webdemo.db:
# sqlalchemy.url = driver://user:pass@localhost/dbname sqlalchemy.url = sqlite:///../../../webdemo.db
注意:實際項目中,數據庫的URL信息是從項目配置文件中讀取,而後經過動態的方式傳遞給Alembic的。具體的作法,讀者能夠參考Magnum項目的實現:https://github.com/openstack/magnum/blob/master/magnum/db/sqlalchemy/migration.py。
如今,咱們能夠建立第一個遷移腳本了,咱們的第一個數據庫版本就是建立咱們的user表:
(.venv)➜ ~/programming/python/webdemo/webdemo/db/sqlalchemy git:(master) ✗ $ alembic revision -m "Create user table" Generating /home/diabloneo/programming/python/webdemo/webdemo/db/sqlalchemy/alembic/versions/4bafdb464737_create_user_table.py ... done
如今腳本已經幫咱們生成好了,不過這個只是一個空的腳本,咱們須要本身實現裏面的具體操做,補充完整後的腳本以下:
"""Create user table Revision ID: 4bafdb464737 Revises: Create Date: 2016-02-21 12:24:46.640894 """ # revision identifiers, used by Alembic. revision = '4bafdb464737' down_revision = None branch_labels = None depends_on = None from alembic import op import sqlalchemy as sa def upgrade(): op.create_table( 'user', sa.Column('id', sa.Integer, primary_key=True), sa.Column('user_id', sa.String(255), nullable=False), sa.Column('name', sa.String(64), nullable=False, unique=True), sa.Column('email', sa.String(255)) ) def downgrade(): op.drop_table('user')
其實就是把User
類的定義再寫了一遍,使用了Alembic提供的接口來方便的建立和刪除表。
咱們須要在webdemo/db/sqlalchemy/目錄下執行遷移操做,可能須要手動指定PYTHONPATH:
(.venv)➜ ~/programming/python/webdemo/webdemo/db/sqlalchemy git:(master) ✗ $ PYTHONPATH=../../../ alembic upgrade head INFO [alembic.migration] Context impl SQLiteImpl. INFO [alembic.migration] Will assume non-transactional DDL. INFO [alembic.migration] Running upgrade -> 4bafdb464737, Create user table
alembic upgrade head
會把數據庫升級到最新的版本。這個時候,在webdemo的根目錄下會出現webdemo.db這個文件,可使用sqlite3命令查看內容:
(.venv)➜ ~/programming/python/webdemo git:(master) ✗ $ ls AUTHORS build ChangeLog dist LICENSE README.md requirements.txt Session.vim setup.cfg setup.py webdemo webdemo.db webdemo.egg-info (.venv)➜ ~/programming/python/webdemo git:(master) ✗ $ sqlite3 webdemo.db SQLite version 3.8.11.1 2015-07-29 20:00:57 Enter ".help" for usage hints. sqlite> .tables alembic_version user sqlite> .schema alembic_version CREATE TABLE alembic_version ( version_num VARCHAR(32) NOT NULL ); sqlite> .schema user CREATE TABLE user ( id INTEGER NOT NULL, user_id VARCHAR(255) NOT NULL, name VARCHAR(64) NOT NULL, email VARCHAR(255), PRIMARY KEY (id), UNIQUE (name) ); sqlite> .header on sqlite> select * from alembic_version; version_num 4bafdb464737
如今咱們能夠把以前使用的內存數據庫換掉,使用咱們的文件數據庫,修改get_engine()
函數:
def get_engine(): global _ENGINE if _ENGINE is not None: return _ENGINE _ENGINE = create_engine('sqlite:///webdemo.db') return _ENGINE
如今你能夠手動往webdemo.db中添加數據,而後測試下API:
➜ ~/programming/python/webdemo git:(master) ✗ $ sqlite3 webdemo.db SQLite version 3.8.11.1 2015-07-29 20:00:57 Enter ".help" for usage hints. sqlite> .header on sqlite> select * from user; sqlite> .schema user CREATE TABLE user ( id INTEGER NOT NULL, user_id VARCHAR(255) NOT NULL, name VARCHAR(64) NOT NULL, email VARCHAR(255), PRIMARY KEY (id), UNIQUE (name) ); sqlite> insert into user values(1, "user_id", "Alice", "alice@example.com"); sqlite> select * from user; id|user_id|name|email 1|user_id|Alice|alice@example.com sqlite> .q ➜ ~/programming/python/webdemo git:(master) ✗ $ ➜ ~/programming/python/webdemo git:(master) ✗ $ curl http://localhost:8080/v1/users {"users": [{"email": "alice@example.com", "user_id": "user_id", "id": 1, "name": "Alice"}]}%
如今,咱們就已經完成了database migration代碼框架的搭建,能夠成功執行了第一個版本的數據庫遷移。OpenStack項目中也是這麼來作數據庫遷移的。後續,一旦修改了項目,須要修改數據模型時,只要新增migration腳本便可。這部分代碼也能夠在https://github.com/diabloneo/webdemo中看到。
在實際生產環境中,當咱們發佈了一個項目的新版本後,在上線的時候,都會自動執行數據庫遷移操做,升級數據庫版本到最新的版本。若是線上的數據庫版本已是最新的,那麼這個操做沒有任何影響;若是不是最新的,那麼會把數據庫升級到最新的版本。
關於Alembic的更多使用方法,請閱讀官方文檔Alembic。
本文到這邊就結束了,這兩篇文章咱們瞭解OpenStack中數據庫應用開發的基礎知識。接下來,咱們將會了解OpenStack中單元測試的相關知識。