1. 程式人生 > >通過demo學習OpenStack開發所需的基礎知識 -- 資料庫(1)

通過demo學習OpenStack開發所需的基礎知識 -- 資料庫(1)

https://segmentfault.com/a/1190000004261891

OpenStack中的資料庫應用主要是關係型資料庫,主要使用的是MySQL資料庫。當然也有一些NoSQL的應用,比如Ceilometer專案。就SQL資料庫本身的應用而言,OpenStack的專案和其他專案並沒有什麼區別,也是採用ORM技術對資料進行增刪改查而已。

本文的重點是講解OpenStack專案中對關係型資料庫的應用的基礎知識,更多的是涉及ORM庫的使用。

資料庫的選擇

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專案的官網是http://www.sqlalchemy.org/,目前該專案最新的版本是1.0.111.0系列是今年剛發的,0.9系列應該還是應用最廣泛的版本。對於一般的應用來說,0.9系列和1.0系列差別不大。


關於SQLAlchemy的學習

我個人覺得SQLAlchemy的學習難度會比Django的Model層難一些,因為一個最簡單的例子也會有一些不太直觀的地方,對於沒用過的人來說,會比較難以理解。不過SQLAlchemy官網整理了一些比較不錯的入門教程,是一個比較好的學習起點:Tutorials。另外,官方的Reference其實是一個很好的教程,講了很多基本的概念,有助於理解SQLAlchemy的庫的使用。Reference的地址是:http://docs.sqlalchemy.org/en/rel_1_0/,還可以直接下載PDF版本。我個人建議大家直接閱讀Reference即可,閱讀順序就按照PDF檔案的章節編排順序進行。雖然這個文件很長,但是我最後發現這麼做是最節約時間的。


SQLAlchemy的架構

先讓我們來看一下SQLAlchemy這個庫的總體架構,如下圖(圖來自官網)所示:

SQLAlchemy這個庫分為兩層:

  • 上面這層是ORM層,為使用者提供ORM介面,即通過操作Python物件來實現資料庫操作的介面。

  • 下面這層是Core層,這層包含了Schema/TypesSQL Expression LanguageEngine這三個部分:

    • 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位置的描述圖:

      • Pool是Engine下面的一個模組,用來管理應用程式到資料庫的連線。

      • 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資料庫時,都是進行同步的序列操作,有效能損失。

為了解決這個問題,社群發起了一次對新驅動的評估,主要是評估PyMySQL驅動: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的名稱,一般可選的值有:postgresqlmysqlsqlite等。driver就是指驅動的名稱,如果不指定,SQLAlchemy會使用預設值。database就是指DBMS中的一個數據庫,一般是指通過CREATE DATABASE語句建立的資料庫。其他的引數就不言而喻了。dialect和driver引數有很多選擇,具體的可以參考官方文件:Database URLs


 mysql+pymysql://XXX:[email protected]/XXX

建立Engine物件

確定了要連線的資料庫資訊後,就可以通過create_engine函式來建立一個Engine物件了。

from sqlalchemy import create_engine

engine = create_engine('sqlite://:memory:')

create_engine函式還支援以下幾個引數:

  • connect_args:一個字典,用來自定義資料庫連線的引數,比如指定客戶端使用的字元編碼。

  • pool_sizemax_overflow:指定連線池的大小。

  • poolclass:指定連線池的實現

  • echo:一個布林值,用來指定是否列印執行的SQL語句到日誌中。

一般來說,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的系統來完成的。


定義資料模型

有了資料庫連線後,我們就可以來定義資料模型了,也就是定義對映資料庫表的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()

class CeilometerBase(object):
    """Base class for Ceilometer Models."""
__table_args__ = _COMMON_TABLE_ARGS
    __table_initialized__ = False
def __setitem__(self, key, value):
        setattr(self, key, value)

    def __getitem__(self, key):
        return getattr(self, key)

    def update(self, values):
        """Make the model object behave like a dict."""
for k, v in six.iteritems(values):
            setattr(self, k, v)


Base = declarative_base(cls=CeilometerBase)

定義對映類

現在我們可以開始建立對映類了。假設我們在資料庫中有一個表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)

def upgrade(self): engine = self._engine_facade.get_engine() models.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)

Base = declarative_base(cls=CeilometerBase)一個是整合Base

輸出結果如下:


...
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

會話

會話(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()

我們通過sessionmakerbind引數把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。