python 64式: 第20式、sqlalchemy进行数据库操作与alembic进行数据库升级

文章目录编排如下:
1 引言
2 使用sqlalchemy实现数据库增删改查
3 使用alembic进行数据库升级
4 总结

1 引言


sqlalchemy是python项目采用的ORM(对象关系映射),主要用于数据库相关的操作。
而alembic是与sqlalchemy搭配使用的数据库升级工具,主要用于数据库相关表结构的修改升级。

基于这篇文章:

python 64式: 第18式、python项目通用rest api框架搭建与编写

的基础上,介绍编写sqlalchemy与alembic部分。

前提:已经创建了项目myproject


2 使用sqlalchemy实现数据库增删改查


2.1 在myproject下面创建db目录
2.2 db目录下创建__init__.py文件,文件内容如下:
#!/usr/bin/env python
# -*- coding: utf-8 -*-

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import retrying
import six.moves.urllib.parse as urlparse
from stevedore import driver

G_NAMESPACE = "myproject.storage"

def getConnectionFromConfig(conf):
    url = conf.database.connection
    connectionScheme = urlparse.urlparse(url).scheme
    mgr = driver.DriverManager(G_NAMESPACE, connectionScheme)
    retries = conf.database.max_retries

    @retrying.retry(wait_fixed=conf.database.retry_interval * 1000,
                    stop_max_attempt_number=retries if retries >= 0 else None)
    def getConnection():
        return mgr.driver(conf)

    connection = getConnection()
    return connection

分析:
1) 这里最重要的代码如下:
mgr = driver.DriverManager(G_NAMESPACE, connectionScheme)
主要是采用stevedore.driver.DriverManager来获取对应的数据库插件。
stevedore.driver.DriverManager :一个名字对应一个entry point。根据插件命名空间和名字,定位到单独插件
stevedore.driver.DriverManager(namespace, name, invoke_on_load, invoke_args=(), invoke_kwds={})
namespace: 命名空间
name: 插件名称
invoke_on_load:如果为True,表示会实例化该插件的类
invoke_args:调用插件对象时传入的位置参数
invoke_kwds:传入的字典参数

2.3 设定数据库插件
修改myproject/setup.cfg中的内容为如下内容:

[metadata]
name = myproject
version = 1.0
summary = myproject
description-file =
    README.rst
author = me
author-email = 
classifier =
    Intended Audience :: Developers
    Programming Language :: Python :: 2.7

[global]
setup-hooks =
    pbr.hooks.setup_hook

[files]
packages =
    myproject
data_files =
    /etc/myproject = etc/myproject/*
    /var/www/myproject = etc/apache2/app.wsgi
    /etc/httpd/conf.d = etc/apache2/myproject.conf

[entry_points]
wsgi_scripts =
    myproject-api = myproject.api.app:build_wsgi_app

console_scripts =
    myproject-dbsync = myproject.cmd.storage:dbsync

oslo.config.opts =
    myproject = myproject.opts:list_opts

myproject.storage =
    mysql = myproject.db.mariadb.impl_mariadb:Connection
    mysql+pymysql = myproject.db.mariadb.impl_mariadb:Connection

分析:
1) 其中最重要的内容是在[entry_points]下面新增了:
myproject.storage =
    mysql = myproject.db.mariadb.impl_mariadb:Connection
    mysql+pymysql = myproject.db.mariadb.impl_mariadb:Connection
来表示myproject.storage的命名空间下,mysql或者mysql+pymysql这个插件名称对应的
处理对象是: myproject.db.mariadb.impl_mariadb下面的Connection类

2) 插件格式
命名空间 = 
  插件名称=模块:可导入对象

2.4 定义数据库模型
在db目录下新建models.py,具体内容如下:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from sqlalchemy import Column
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Index
from sqlalchemy import Integer
from sqlalchemy import PrimaryKeyConstraint
from sqlalchemy import String

Base = declarative_base()

class Student(Base):
    __tablename__ = 'student'
    id = Column(Integer(), nullable=False)
    userId = Column(String(256), nullable=False)
    name = Column(String(256), nullable=False)
    age = Column(Integer(), nullable=True)
    email = Column(String(256), nullable=True)
    __table_args__ = (
        Index('ix_student_userId', 'userId'),
        PrimaryKeyConstraint('id')
    )

    TRANSFORMED_FIELDS = ['id', 'userId', 'name', 'age', 'email']
    # from database model to dict
    def as_dict(self):
        result = dict()
        for field in Student.TRANSFORMED_FIELDS:
            result[field] = getattr(self, field, '')
        return result

分析:
1) as_dict方法主要是用于将models.Student对象转换为字典

2.5 为请求绑定database hook, config hook
2.5.1 在myproject的api目录下新建hooks.py
具体内容如下:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from pecan import hooks

class ConfigHook(hooks.PecanHook):
    """Attach config information to the request
    
    """
    def __init__(self, conf):
        self.conf = conf

    def before(self, state):
        state.request.cfg = self.conf

class DatabaseHook(hooks.PecanHook):
    """Attach database information to the request
    
    """
    def __init__(self, conn):
        self.storage = conn

    def before(self, state):
        state.request.storage = self.storage

分析:
1) 这个hook在每个请求进来的时候实例化一个db的Connection对象,然后在controller代码中我们可以直接使用这个Connection实例
具体参考:
https://pecan.readthedocs.io/en/latest/hooks.html
hook的作用:
pecan hooks是一种可以与框架交互的方式,不需要编写单独的中间件。
Hooks允许你在请求生命周期的关键时间点去执行代码:

on_route(): called before Pecan attempts to route a request to a controller
before(): called after routing, but before controller code is run
after(): called after controller code has been run
on_error(): called when a request generates an exception

参考:
https://www.sqlalchemy.org/library.html#tutorials
https://segmentfault.com/a/1190000004466246

2)修改myproject的api目录下app.py
内容为如下内容:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os

from oslo_config import cfg
from oslo_log import log
from paste import deploy
import pecan

from myproject.api import hooks
from myproject import service
from myproject import db

PECAN_CONFIG = {
    'app': {
        'root': 'myproject.api.controllers.root.RootController',
        'modules': ['myproject.api'],
    },
}

LOG = log.getLogger(__name__)


def app_factory(global_config, **local_config):
    print "######### enter app_factory"
    conf = service.prepareService()

    # NOTE, add config and databse information to the request
    # by using pecan.hooks.PecanHook
    configHook = hooks.ConfigHook(conf)
    conn = db.getConnectionFromConfig(conf)
    databaseHook = hooks.DatabaseHook(conn)
    appHooks = [configHook, databaseHook]

    # NOTE, it needs add the line below
    pecan.configuration.set_config(dict(PECAN_CONFIG), overwrite=True)
    app = pecan.make_app(
        PECAN_CONFIG['app']['root'],
        hooks=appHooks
    )
    return app

def getUri(conf):
    # TODO(), it needs to get real path of api-paste.ini
    # the path is setted by the data_files under [files] in setup.config
    # configPath = "/etc/myproject/api-paste.ini"

    # find the absolute path of api-paste.ini
    cfgFile = None
    apiPastePath = conf.api.paste_config
    if not os.path.isabs(apiPastePath):
        cfgFile = conf.find_file(apiPastePath)
    elif os.path.exists(apiPastePath):
        cfgFile = apiPastePath
    if not cfgFile:
        raise cfg.ConfigFilesNotFoundError([conf.api.paste_config])
    LOG.info("The wsgi config file path is: %s" % (cfgFile))
    result = "config:" + cfgFile
    return result

def getAppName():
    return "main"

def build_wsgi_app():
    print "######### enter build_wsgi_app"
    conf = service.prepareService()
    uri = getUri(conf)
    appName = getAppName()
    app = deploy.loadapp(uri, name=appName)
    return app

分析:
1) 在app_factory方法中,设置了
    configHook = hooks.ConfigHook(conf)
    conn = db.getConnectionFromConfig(conf)
    databaseHook = hooks.DatabaseHook(conn)
    appHooks = [configHook, databaseHook]

    # NOTE, it needs add the line below
    pecan.configuration.set_config(dict(PECAN_CONFIG), overwrite=True)
    app = pecan.make_app(
        PECAN_CONFIG['app']['root'],
        hooks=appHooks
    )
主要的目的就是让请求进入的时候可以绑定数据库连接对象和配置对象,方便后面进行处理

2.6 编写数据库连接的基类
在db目录下新建base.py,具体内容如下:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import abc

import six

@six.add_metaclass(abc.ABCMeta)
class Connection(object):

    def __init__(self, conf, url):
        pass

    def upgrade(self):
        """ Migrate database """

    def getStudents(self):
        """ Get student list """

    def createStudent(self):
        """ Create a student"""

    def updateStudent(self):
        """  Update a student"""

    def deleteStudent(self):
        """ Delere a student """


2.7 实现数据库连接的子类
在db目录下新建mariadb目录,
2.7.1 在mariadb目录下新建__init__.py
内容为空

2.7.2 在mariadb目录下新建impl_mariadb.py
内容具体如下所示:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os

from alembic import command
from alembic import config
from alembic import migration
from oslo_db.sqlalchemy import session as dbSession

from myproject.db import base
from myproject.db import models

class Connection(base.Connection):
    def __init__(self, conf):
        self.conf = conf
        options = dict(conf.database)
        options['max_retries'] = 0
        self.engineFacade = dbSession.EngineFacade(
            conf.database.connection,
            **options)

    def getAlembicConfig(self):
        dirName = os.path.dirname(__file__)
        realDirName = os.path.dirname(dirName)
        path = "%s/migrate/alembic.ini" % (realDirName)
        cfg = config.Config(path)
        cfg.set_main_option(
            'sqlalchemy.url', self.conf.database.connection)
        return cfg

    def upgrade(self, noCreate=False):
        # import pdb;pdb.set_trace()
        cfg = self.getAlembicConfig()
        cfg.conf = self.conf
        if noCreate:
            command.upgrade(cfg, "head")
        else:
            engine = self.engineFacade.get_engine()
            ctxt = migration.MigrationContext.configure(engine.connect())
            currentVersion = ctxt.get_current_revision()
            if currentVersion is None:
                models.Base.metadata.create_all(engine, checkfirst=False)
                command.stamp(cfg, "head")
            else:
                command.upgrade(cfg, "head")

    @staticmethod
    def rowToStudentModel(row):
        result = models.Student(
            id=row.id,
            userId=row.userId,
            name=row.name,
            age=row.age,
            email=row.email
        )
        return result

    def retrieveStudents(self, query):
        return (self.rowToStudentModel(obj) for obj in query.all())

    def getStudents(self, id=None, userId=None, name=None,
                    age=None, email=None, pagination=None):
        # NOTE, pagination is needed
        pagination = pagination or {}
        session = self.engineFacade.get_session()
        query = session.query(models.Student)
        if id is not None:
            query = query.filter(models.Student.id == id)
        if userId is not None:
            query = query.filter(models.Student.userId == userId)
        if name is not None:
            query = query.filter(models.Student.name == name)
        if age is not None:
            query = query.filter(models.Student.age == age)
        if email is not None:
            query = query.filter(models.Student.email == email)
        # TODO(), add pagination query
        students = self.retrieveStudents(query)
        return students

    def createStudent(self, obj):
        session = self.engineFacade.get_session()
        row = models.Student(
            userId=obj.userId,
            name=obj.name,
            age=obj.age,
            email=obj.email
        )
        with session.begin():
            session.add(row)
        return row

    def updateStudent(self, obj):
        session = self.engineFacade.get_session()
        with session.begin():
            count = session.query(models.Student).filter(
                models.Student.id == obj.id).update(
                obj.as_dict()
            )
            if not count:
                raise "Not found student with id: %s" % obj.id
        return obj

    def deleteStudent(self, id):
        session = self.engineFacade.get_session()
        with session.begin():
            session.query(models.Student).filter(
                models.Student.id == id).delete()

分析:
1) upgrade(self, nocreate=False):
作用: 通过alembic判断是否有版本,如果没有版本就创建所有的表,并升级到最新版本;
  如果有版本就直接升级到最新版本
处理过程:
步骤1: 获取alembic的配置对象,设置alembic.ini中sqlalchemy.url为当前数据库连接串
步骤2: 如果不创建表,就直接升级到最新版本;否则,执行步骤3
步骤3: 升级数据库版本,具体如下:
    步骤3.1: 根据 oslo_db.sqlalchemy.session.EngineFacade对象获取engine
    步骤3.2: 获取enine.connect()做为参数传递给alembic.migration.MigrationContext.configure来得到
      数据库迁移上下文对象
    步骤3.3: 获取数据库迁移上下文对象的当前版本
      步骤3.3.1: 如果当前版本不存在,就通过models.Base.metadata.create_all(engine)创建所有表,
         并通过alembic.command.stamp(cfg, 'head')给当前版本打上标记
         否则,执行3.3.2
      步骤3.3.2: 直接执行alembic.command.upgrade(cfg, 'head')升级到最新版本

2) _get_alembic_config(self):
作用: 初始化alembic.ini的配置对象,并设置alembic.ini中sqlalchemy.url的值为配置文件中[database]下面
的connection对应的值,返回该配置对象

3) oslo_db.sqlalchemy.session.EngineFacade
作用: 
1 类似SQLAlchemy Engine和Session对象的网关(外观模式,类似一个代理)。
得到该对象后
2 管理数据库连接,会话,事务
本质: 单例,具体参见C++如何获取一个单例的写法
用法:
1 init函数初始化获得
_FACADE = None

def _create_facade_lazily():
    global _FACADE
    if _FACADE is None:
        _FACADE = db_session.EngineFacade(
        CONF.database.connection,
        **dict(CONF.database.iteritems())
        )
    return _FACADE

2 从配置文件初始化获得
_ENGINE_FACADE = None
_LOCK = threading.Lock()

def _create_facade_lazily():
    global _LOCK, _ENGINE_FACADE
    if _ENGINE_FACADE is None:
        with _LOCK:
        if _ENGINE_FACADE is None:
            _ENGINE_FACADE = db_session.EngineFacade.from_config(CONF)
    return _ENGINE_FACADE
                
参考:
https://specs.openstack.org/openstack/oslo-specs/specs/kilo/make-enginefacade-a-facade.html
https://blog.csdn.net/bill_xiang_/article/details/78592389
 

3 使用alembic进行数据库升级


3.1 在myproject下新建cmd目录
cmd目录存放各个脚本或者服务的启动代码。
3.1.1 在cmd目录下新建__init__.py文件
内容设置为空

3.1.2 在cmd目录下新建storage.py文件
内容设置为如下内容:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from myproject import service
from myproject import db

def dbsync():
    conf = service.prepareService()
    db.getConnectionFromConfig(conf).upgrade()

分析:
1) service.prepareService()是获取了oslo.config对象
2) db.getConnectionFromConfig(conf).upgrade()
这句话就是读取数据库连接串根据调用对应的数据库插件对象的upgrade方法来
进行数据库升级
3) 数据库升级的代码实际就是myproject/db/mariadb/impl_mariadb.py
的Connection类的upgrade方法,具体如下:
    def upgrade(self, noCreate=False):
        # import pdb;pdb.set_trace()
        cfg = self.getAlembicConfig()
        cfg.conf = self.conf
        if noCreate:
            command.upgrade(cfg, "head")
        else:
            engine = self.engineFacade.get_engine()
            ctxt = migration.MigrationContext.configure(engine.connect())
            currentVersion = ctxt.get_current_revision()
            if currentVersion is None:
                models.Base.metadata.create_all(engine, checkfirst=False)
                command.stamp(cfg, "head")
            else:
                command.upgrade(cfg, "head")


3.2 设定数据库升级脚本
具体参见myproject下的setup.cfg中
[entry_points]
下有如下一行内容
console_scripts =
    myproject-dbsync = myproject.cmd.storage:dbsync
这表示会生成一个myproject-dbsync的脚本,调用
myproject/cmd/storage.py中的dbsync方法,
该方法的具体定义请参见3.1.2


3.3 创建alembic配置
3.3.1 alembic生成数据库迁移环境
进入到db/sqlalchemy目录下执行
alembic init alembic

3.3.2 进入到db/sqlalchemy目录下执行
alembic revision -m "Create student table"

在db/sqlalchemy/alembic/versions 目录下生成一个文件
xxx_create_student_table.py

然后向该文件中填入创建表操作的信息,具体样例信息内容如下:

"""Create student table

Revision ID: 677de41920e9
Revises: 


"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = '677de41920e9'
down_revision = None
branch_labels = None
depends_on = None

def upgrade():
    op.create_table(
        'student',
        sa.Column('id', sa.Integer(), nullable=False),
        sa.Column('userId', sa.String(256), nullable=False),
        sa.Column('name', sa.String(128), nullable=False),
        sa.Column('age', sa.Integer(), nullable=True),
        sa.PrimaryKeyConstraint('id')
    )
    op.create_index(
        'ix_student_userId', 'student', ['userId'], unique=True)


def downgrade():
    op.drop_table('student')

解释:
alembic init :创建一个alembic仓库
alembic revision -m '提交信息': 创建一个新的版本文件
alembic upgrade: 升级命令

参考:
https://www.baidu.com/link?url=Aad8bvURqUkaCJxNXjmIRKdzKE5XZrP02j_m9nndmJdhW6NE-p5jRAWYU5xY3HW5gQu9LlI52mSwJCFvniNWwq&wd=&eqid=da2ccd990001d02d000000055bfe26a4

3.4 生成myproject-dbsync文件
具体在myproject目录下执行:
python setup.py install

如果是centos环境,那么会在/usr/bin目录下生成myproject-dbsync文件

3.5 修改配置文件中的数据库连接串
3.5.1 生成配置文件(如果有,就不要执行这一步)
找到myproject-config-generator.conf文件,该文件内容如下:

[DEFAULT]
output_file = /etc/myproject/myproject.conf
wrap_width = 79
namespace = myproject
namespace = oslo.db
namespace = oslo.log
namespace = oslo.messaging
namespace = oslo.policy

执行命令:
oslo-config-generator --config-file=myproject-config-generator.conf
生成myproject.conf文件,

3.5.2 找到myproject.conf里面的
[database]
下面的
# The SQLAlchemy connection string to use to connect to the database. (string
# value)
# Deprecated group/name - [DEFAULT]/sql_connection
# Deprecated group/name - [DATABASE]/sql_connection
# Deprecated group/name - [sql]/connection
#connection = <None>
添加如下一行内容:
connection = mysql+pymysql://myproject:[email protected]/myproject?charset=utf

3.6 修改alembic配置文件
修改alembic相关文件如下:
3.6.1 alembic/env.py中的内容:
添加:
from myproject.db import models
修改
target_metadata = None

target_metadata = models.Base.metadata
解释:
这样做的原因是数据库迁移需要知道具体对应数据库的信息,
这里是设置数据库的元信息为当前应用的数据库信息

参考:
https://alembic.sqlalchemy.org/en/latest/autogenerate.html

3.6.2 alembic.ini中内容:

[alembic]
# path to migration scripts
# TODO(), edit it as real path
script_location = alembic
修改为:
[alembic]
# path to migration scripts
# TODO(), edit it as real path
script_location = myproject.db.migrate:alembic

解释:
这里主要是指定当前项目迁移脚本的路径,需要根据具体的项目路径进行替换

参考:
https://alembic.sqlalchemy.org/en/latest/tutorial.html

将默认的sqlalchemy.url修改为如下内容
sqlalchemy.url =
解释:
这里原本主要是配置数据库连接串的,因为在代码中配置了升级方法upgrade()
中通过如下方法中的set_main_option重新设置了sqlalchemy.url,所以
这里替换成空值。


3.6.3 请确保alembic目录和alembic.ini在同级目录
样例如下:
[root@localhost migrate]# tree
.
|-- alembic
|   |-- env.py
|   |-- env.pyc
|   |-- __init__.py
|   |-- README
|   |-- script.py.mako
|   `-- versions
|       |-- 677de41920e9_create_student_table.py
|       |-- 677de41920e9_create_student_table.pyc
|       |-- 77cca51ca78e_add_email_to_student.py
|       `-- __init__.py
|-- alembic.ini
`-- __init__.py

然后确保:
myproject/db/mariadb/impl_mariadb.py中Connection类的getAlembicConfig
方法中内容如下:
    def getAlembicConfig(self):
        dirName = os.path.dirname(__file__)
        realDirName = os.path.dirname(dirName)
        path = "%s/migrate/alembic.ini" % (realDirName)
        cfg = config.Config(path)
        cfg.set_main_option(
            'sqlalchemy.url', self.conf.database.connection)
        return cfg

注意:
请确保:
path = "%s/migrate/alembic.ini" % (realDirName)
这个路径是正确的,请根据实际路径进行修改

3.7 创建数据库
执行如下命令来创建数据库:
mysql -e "create database IF NOT EXISTS myproject"
mysql -e "GRANT ALL PRIVILEGES ON myproject.* TO 'myproject'@'localhost' IDENTIFIED BY 'password';"
mysql -e "GRANT ALL PRIVILEGES ON myproject.* TO 'myproject'@'%' IDENTIFIED BY 'password';"

3.8 第一次数据库同步
3.8.1 修改myproject/db/models.py中内容为如下:
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from sqlalchemy import Column
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Index
from sqlalchemy import Integer
from sqlalchemy import PrimaryKeyConstraint
from sqlalchemy import String

Base = declarative_base()

class Student(Base):
    __tablename__ = 'student'
    id = Column(Integer(), nullable=False)
    userId = Column(String(256), nullable=False)
    name = Column(String(256), nullable=False)
    age = Column(Integer(), nullable=True)
    # email = Column(String(256), nullable=True)
    __table_args__ = (
        Index('ix_student_userId', 'userId'),
        PrimaryKeyConstraint('id')
    )

    TRANSFORMED_FIELDS = ['id', 'userId', 'name', 'age', 'email']
    # from database model to dict
    def as_dict(self):
        result = dict()
        for field in Student.TRANSFORMED_FIELDS:
            result[field] = getattr(self, field, '')
        return result

注意: 这里先注释了email字段,后续以升级形式添加email字段到student表中


3.8.2 数据库同步
执行:
/usr/bin/myproject-dbsync
注意: 不同环境下生成的yproject-dbsync路径不同,请根据实际情况进行处理,这里是centos环境的

进入mysql,执行如下命令:
use myproject;
show tables;
可以看到:
MariaDB [myproject]> select * from alembic_version;
+--------------+
| version_num  |
+--------------+
| 677de41920e9 |

MariaDB [myproject]> desc student;
+--------+--------------+------+-----+---------+----------------+
| Field  | Type         | Null | Key | Default | Extra          |
+--------+--------------+------+-----+---------+----------------+
| id     | int(11)      | NO   | PRI | NULL    | auto_increment |
| userId | varchar(256) | NO   | MUL | NULL    |                |
| name   | varchar(256) | NO   |     | NULL    |                |
| age    | int(11)      | YES  |     | NULL    |                |
+--------+--------------+------+-----+---------+----------------+
4 rows in set (0.00 sec)


3.9 数据库升级
3.9.1  修改myproject/db/models.py中内容为如下:
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from sqlalchemy import Column
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Index
from sqlalchemy import Integer
from sqlalchemy import PrimaryKeyConstraint
from sqlalchemy import String

Base = declarative_base()

class Student(Base):
    __tablename__ = 'student'
    id = Column(Integer(), nullable=False)
    userId = Column(String(256), nullable=False)
    name = Column(String(256), nullable=False)
    age = Column(Integer(), nullable=True)
    email = Column(String(256), nullable=True)
    __table_args__ = (
        Index('ix_student_userId', 'userId'),
        PrimaryKeyConstraint('id')
    )

    TRANSFORMED_FIELDS = ['id', 'userId', 'name', 'age', 'email']
    # from database model to dict
    def as_dict(self):
        result = dict()
        for field in Student.TRANSFORMED_FIELDS:
            result[field] = getattr(self, field, '')
        return result

注意: 这里取消注释email字段,验证升级后email字段会添加到student表中

3.9.2 生成版本迁移文件
这里在myproject/db/migrate/alembic/versions目录下生成 77cca51ca78e_add_email_to_student.py 文件
其中: 77cca51ca78e 就是版本号
具体内容如下:

"""add email to student

Revision ID: 77cca51ca78e
Revises: 677de41920e9

"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = '77cca51ca78e'
down_revision = '677de41920e9'
branch_labels = None
depends_on = None


def upgrade():
    op.add_column('student', sa.Column('email', sa.String(256), nullable=True))


def downgrade():
    op.drop_column('student', 'email')

分析:
1) down_revision 就是上一个版本的版本号,请根据实际情况修改
2) upgrade()定义了数据库升级的操作,这里是添加email字段到student表中
3) upgrade()定义了数据库降级的操作,这里是从student表中删除email字段

3.9.3 再次执行/usr/bin/myproject-dbsync
结果如下:

MariaDB [myproject]> desc student;
+--------+--------------+------+-----+---------+----------------+
| Field  | Type         | Null | Key | Default | Extra          |
+--------+--------------+------+-----+---------+----------------+
| id     | int(11)      | NO   | PRI | NULL    | auto_increment |
| userId | varchar(256) | NO   | MUL | NULL    |                |
| name   | varchar(256) | NO   |     | NULL    |                |
| age    | int(11)      | YES  |     | NULL    |                |
| email  | varchar(256) | YES  |     | NULL    |                |
+--------+--------------+------+-----+---------+----------------+
5 rows in set (0.00 sec)

MariaDB [myproject]> select * from alembic_version;
+--------------+
| version_num  |
+--------------+
| 77cca51ca78e |
+--------------+

验证升级成功,且版本更新到最新

3.10 alembic常用api
alembic命令解释

3.10.1
alembic.op.create_table(table_name, *columns, **kws)
作用: 使用当前迁移上下文来执行一个创建表的命令
参数:
table_name: 表名
columns: 数组, 每个元素都是一个sqlalchemy.Column列对象,
         可能还包括sqlalchemy.Index对象和sqlalchemy.PrimaryKeyConstraint对象等
kws: 字典参数,例如: mysql_engine='InnoDB', mysql_charset='utf8'等

3.10.2 
alembic.op.create_index(index_name, table_name, columns, schema=None, unique=False, **kw)
作用: 使用当前迁移上下文来创建一个索引
参数:
index_name: 索引名称
table_name: 表名
columns: 列名的数组
unique: 如果为True,创建一个唯一的索引


3.10.3 
sqlalchemy.PrimaryKeyConstraint(*columns, **kw)
作用: 设置表的主键
参数:
columns: 列名数组,即要设置为主键的一个或多个列名组成的数组
kw: 字典参数

3.10.4
sqlalchemy.UniqueConstraint(*columns, **kw)
作用: 设置表的唯一性索引。即确保某个列或某几个列的值在表中是唯一的。
      往往用于列去重。
参数:
columns: 列名数组,即要设置为主键的一个或多个列名组成的数组  
kw: 字典参数

用法示例:
sqlalchemy.UniqueConstraint('name','addr',name='unique_mytable_name_addr')

3.10.5
sqlalchemy.Column(*args, **kwargs)
作用: 表的列名及相关属性设置
参数:
args: 数组参数,例如: 列名,列字段类型
kwargs: 字典参数,例如: 是否可以为空等。
nullable: 为True表示该列可为空;为False表示该列不可以为空

3.10.6
alembic.op.add_column(table_name, column, schema=None)
作用: 根据当前迁移上下文环境添加某个列
参数:
table_name:表名
column: sqlalchemy.Column列对象

alembic.op.drop_column(table_name, column_name, schema=None, **kw)
作用: 根据当前迁移上下文环境删除某列
参数:
table_name: 表名
column_name: 列名,字符串类型

alembic.op.alter_column(table_name, column_name, nullable=None, 
server_default=False, new_column_name=None, type_=None, 
existing_type=None, existing_server_default=False, 
existing_nullable=None, schema=None, **kw)

参考:
https://alembic.sqlalchemy.org/en/latest/ops.html#alembic.operations.Operations.create_table
http://alembic.zzzcomputing.com/en/latest/ops.html
https://alembic.sqlalchemy.org/en/latest/tutorial.html

3.11 其他问题
3.11.1 丢失alembic错误
执行:
/usr/bin/myproject-dbsync

报错:
2018-11-28 14:52:26.357 31559 ERROR myproject   File "/usr/lib/python2.7/site-packages/myproject/db/mariadb/impl_mariadb.py", line 12, in <module>
2018-11-28 14:52:26.357 31559 ERROR myproject     from myproject.db import models
2018-11-28 14:52:26.357 31559 ERROR myproject   File "/usr/lib/python2.7/site-packages/myproject/db/models.py", line 4, in <module>
2018-11-28 14:52:26.357 31559 ERROR myproject     from sqlalchemy import Column
2018-11-28 14:52:26.357 31559 ERROR myproject ImportError: cannot import name Column

分析:
丢失amebic文件

尝试:
package_data={
    'package': ['./path/*.xsd'],
},
字典的键必须是你的真实包名,值必须是要包含的模式的列表。要包含Package 2/http/api/api.yaml:

package_data={
    'package2': ['http/api/*.yaml'],
},
列出所有非python文件和模式。

另一种方法是创建MANIFEST.in(通常用于源分发)和https://setuptools.readthedocs.io/en/latest/setuptools.html#including-data-files

include_package_data=True,
setup()。

参考:
https://cloud.tencent.com/developer/ask/143280
https://blog.csdn.net/s1234567_89/article/details/53008444
https://docs.python.org/2/distutils/sourcedist.html#the-manifest-in-template

setup.cfg设置package_data

最终解决办法:
在setup.cfg中修改为如下内容:
import setuptools

setuptools.setup(
    # FIXME(), if not add package_data and include_package_data
    # some files which ends with .ini or other will be missed
    package_data={
        # If any package contains *.ini or *.txt files, include them:
        '': ['*.ini', '*.mako', '*.yaml', '*.txt', '*.py', 'README', '*.json', '*.wsgi'],
    },
    include_package_data=True,
    setup_requires=['pbr'],
    pbr=True)


3.11.2 数据库连接失败
ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/var/lib/mysql/mysql.sock' (2 "No such file or directory")

解决方法:
[root@localhost alembic]# systemctl status mariadb
● mariadb.service - MariaDB 10.1 database server
   Loaded: loaded (/usr/lib/systemd/system/mariadb.service; disabled; vendor preset: disabled)
   Active: inactive (dead)
[root@localhost alembic]# systemctl restart mariadb

解决

4 总结


python项目中通过sqlalchemy来操作数据库,进行增删改查等;通过alembic进行数据库的升级
 

猜你喜欢

转载自blog.csdn.net/qingyuanluofeng/article/details/84782169