使用SQLAlchemy创建数据模型

50 篇文章 23 订阅

https://blog.csdn.net/happyanger6/article/details/53947162

如前所述,模型(models)是对数据抽象并提供通用访问接口的一种方式。在大多数网络应用中,数据会被存储在一个关系数据库管理系统(RDBMS)中,也就是把数据格式化存储在由行与列组成的表格中,且能够跨表对数据进行比较。例如MySQL,Postgres,Oracle,MSSQL。

为了基于数据库抽象出数据模型,我们需要使用一个叫作SQLAlchemy的Python包。SQLAlchemy在最底层包装了数据库操作接口,在最上层提供了对象关系映射(ORM)。ORM是在基于不同的数据结构和系统类型的数据源之间传递和转换数据的技术。在这里,它用来把大量不同类型的数据库中的数据,转换成python对象的集合。同时,像Python这样的语言,允许你在不同的对象之间建立引用,读取和设置它们的属性;而SQLAlchemy这样的ORM,能为你将对象操作转换为传统的数据库操作。

为了把SQLAlchemy绑定到我们的应用上下文中,我们可以使用Flask SQLAlchemy。它在SQLAlchemy上提供了一层包装,这样就可以结合Flask的一些特性来方便地调用SQLAlchemy的功能。如果你对SQLAlchemy已经很熟悉,那么你可以单独使用它,而无须和Flask SQLAlchemy一起使用。

在本篇教程最后,我们会为博客应用准备完整的数据库结构,以及与之交互的数据模型。

设置SQLAlchemy

我们需要先选择一个数据库。这里选择SQLite.

SQLite是无须运行服务的SQL数据库。它的运行速度很快,所有数据都包含在一个文件中,而且支持Python。

Python安装包

使用pip安装Flask SQLAlchemy,运行以下命令:

pip3 install flask-sqlalchemy

Flask SQLAlchemy

在开始抽象数据结构之前,我们需要先设置Flask SQLAlchemy。SQLAlchemy通过一个特殊的数据库URI来创建数据库连接,这个URI是一个类似于URL的字符串,包含了SQLAlchemy建立连接所需的所有信息。下面是它的一般形式:

databasetype+driver://user:password@ip:port/db_name

对于你在之前安装的每个驱动程序来说,对应的URI会是:

#SQLite

sqlite:///database.db

#MySQL

mysql+pymysql://user:password@ip:port/db_name

#Postgres

postgresql+psycopg2://user:password@ip:port/db_name

#MSSQL

mssql+pyodbc://user:password@dsn_name

#Oracle

oracle+cx_oracle://user:password@ip:port/db_name

在我们的config.py文件中将URI添加到DevConfig中:


class DevConfig(Config):
    debug = True
    SQLALCHEMY_DATABASE_URI = "sqlite:///database.db"

我们的第一个数据模型

我们还没有真正进入数据库中去创建任何表结构。SQLAlchemy不但允许我们根据数据库表结构创建数据模型(model),也允许我们根据数据模型来创建数据库表结构。所以当我们把第1个模型创建出来之后,表结构也就有了。

首先,要在main.py文件中将我们的app对象传给SQLAlchemy,将SQLAlchemy初始化:


from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config.from_object(DevConfig)
db = SQLAlchemy(app)

SQLAlchemy会从app的配置中读取信息,自动连接到数据库。我们首先在main.py中创建一个User模型,它会跟相应的一个user表进行交互。


class User(db.Model):
    id = db.Column(db.Integer(), primary_key=True)
    username = db.Column(db.String(255))
    password = db.Column(db.String(255))

    def __init__(self, username):
        self.username = username

    def __repr__(self):
        return "<User `{}`>".format(self.username)
这段代码做了什么呢?实际上,我们已经得到了User模型,它基于一个user表,该表拥有3个字段。当我们继承db.Model时,与数据库连接和通信的工作已经自动完成了。User的某些类的属性值是db.Column类的实例,每个这样的属性都代表了数据库表里的一个字段。在db.Column的构造函数里,第1个参数是可选的,通过这个参数我们可以指定该属性在数据库中的字段名。如果没有指定,则SQLAlchemy会认为字段名与这个属性的名字是一样的。如果要指定这个可选参数,则可以这样写:
username = db.Column('user_name', db.String(255))

传给db.Column的第2个参数会告诉SQLAlchemy,应该把这个字段作为什么类型来处理。我们在书中将会用到的主要类型有:

db.String
db.Text
db.Integer
db.Float
db.Boolean
db.Date
db.DateTime
db.Time
每种类型的含义都很简单。String和Text类型会接收Python的字符串,并且把它们转为varchar和text类型的字段。Integer和Float类型则会接收Python的任意数值类型,把它们分别转换为对应的正确类型,再插入数据库中。Boolean类型会接收Python的布尔值转换成Boolean类型的字段;如果数据库不支持boolean类型的字段,则SQLAlchemy会自动把Python的布尔值转换为0和1保存在数据库中。Date,DateTime和Time类型使用了Python的datetime原生包中的同名类,并把它们转换后保存到数据库中。String,Integer和Float类型都会接收一个额外的参数,来告诉SQLAlchemy该字段的存储长度限制。

primary_key参数会告诉SQLAlchemy,这个字段需要做主键索引。每个SQLAlchemy模型类型都必须有一个主键才能正常工作。

SQLAlchemy会假设你的表名就模型类型的小写版本。但是,如果你想给表起个别的名字,你可以给类添加'__tablename__'的类属性。另外,通过采用这种方式,你也可以使用在数据库中已经存在的表,只需把表名设为该属性的值:


class User(db.Model):
    __tablename__ = 'user_table_name'

   id = db.Column(db.Integer(), primary_key=True)
    username = db.Column(db.String(255))
    password = db.Column(db.String(255))
我们不需要定义__init__或__repr__方法,如果我们没有定义,则SQLAlchemy会自动创建__init__方法。你定义的所有字段名将会成为此方法所接收的关键字的参数名。


创建user表

现在有SQLAlchemy完成繁重的劳动,我们就可以轻松地在数据库中创建user表了。更新manage.py如下:


from flask_script import Manager, Server
from main import app, db, User

......
manager = Manager(app)
manager.add_command("server", Server())

@manager.shell
def make_shell_context():
    return dict(app=app, db=db, User=User)
注意:每新增一个模型,都会在这个地方导入并添加到dict中。


这样就能够在命令行中使用我们的模型了。现在可以运行命令行,并用db.create_all()来创建所有的表:

Z:\python\flask_tutorials>python manage.py shell
>>> db.create_all()
2016-12-30 22:58:44,661 INFO sqlalchemy.engine.base.Engine SELECT CAST('test pla
in returns' AS VARCHAR(60)) AS anon_1
2016-12-30 22:58:44,662 INFO sqlalchemy.engine.base.Engine ()
2016-12-30 22:58:44,663 INFO sqlalchemy.engine.base.Engine SELECT CAST('test uni
code returns' AS VARCHAR(60)) AS anon_1
2016-12-30 22:58:44,663 INFO sqlalchemy.engine.base.Engine ()
2016-12-30 22:58:44,664 INFO sqlalchemy.engine.base.Engine PRAGMA table_info("us
er")
2016-12-30 22:58:44,664 INFO sqlalchemy.engine.base.Engine ()
2016-12-30 22:58:44,667 INFO sqlalchemy.engine.base.Engine
CREATE TABLE user (
        id INTEGER NOT NULL,
        username VARCHAR(255),
        password VARCHAR(255),
        PRIMARY KEY (id)
)

你现在应该能在数据库中找到一个叫user的表,该表中有你所指定的那些字段。同样,如果你使用的是SQLite,则也会在你的目录结构中找到一个叫作database.db的文件。

CRUD

在每种数据存储策略中,都存在4个基本功能类型:添加,读取,修改和删除(CRUD)。CRUD提供了我们的网络应用中需要的所有操作和检视数据的基础功能。要使用这些功能,我们需要在数据库中用到一个叫作会话(session)的对象。会话的含义会在稍后进行解释,但是现在可以先把它们看作保存对数据库的改动的地方。

新增数据

要使用我们的数据库模型在数据库中新增一条记录,可以把数据添加到会话对象中,并将其提交。在会话中添加一个对象,这个改动将在会话中被标记为待保存。而提交则可以把这个会话的改动保存进数据库。代码如下:

>>> user = User(username='fake_name')
>>> db.session.add(user)
>>> db.session.commit()
2016-12-30 23:07:25,465 INFO sqlalchemy.engine.base.Engine BEGIN (implicit)
2016-12-30 23:07:25,467 INFO sqlalchemy.engine.base.Engine INSERT INTO user (use
rname, password) VALUES (?, ?)
2016-12-30 23:07:25,467 INFO sqlalchemy.engine.base.Engine ('fake_name', None)
2016-12-30 23:07:25,486 INFO sqlalchemy.engine.base.Engine COMMIT

读取数据

把数据添加进数据库后,SQLAlchemy可以通过Model.query方法对数据进行查询。Model.query是db.session.query(Model)的简写。

下面是第一个例子,使用all()获取数据库中的所有行,并作为列表返回。

>>> users = User.query.all()
>>> users
[<User `fake_name`>]
>>>

当数据库中的记录数量越来越多时,查询操作就会变慢。同使用SQL一样,在SQLAlchemy里,我们可以使用limit函数来指定希望返回的总行数:

>>> users = User.query.limit(10).all()
>>> users
[<User `fake_name`>]

在默认情况下,SQLAlchemy会根据主键排序并返回记录。要控制排序的方式,我们可以使用order_by函数,使用方法如下:

#正向排序

>>> users = User.query.order_by(User.username).all()
>>> users
[<User `fake_name`>, <User `zhangxa`>]

#逆向排序

>>> users = User.query.order_by(User.username.desc()).all()
>>> users
[<User `zhangxa`>, <User `fake_name`>]

如果只想返回一行数据,则可以使用first()来替代all():

>>> user = User.query.first()
>>> user
<User `fake_name`>

要通过主键取得一行数据,则可以使用query.get():

>>> user = User.query.get(1)
>>> user
<User `fake_name`>

所有的这些函数都是可以链式调用的,也就是说,可以把它们追加在一起,来修改最终的返回结果。我们如果精通JavaScript,则会对这样的语法非常熟悉:

>>> users = User.query.order_by(User.username.desc()).limit(10).first()
>>> users
<User `zhangxa`>

另外,还存在一个Flask SQLAlchemy专有的方法,叫作pagination(分页),可以用来替代first()和all()。这个方法是专门设计用来实现分页功能的,大多数网站都会用分页的方式来展示长列表。第1个参数指示查询应该返回第几页的内容,第2个参数是每页展示的对象数量。所以,如果我们传入1和10作为参数,则会获得前10个对象作为返回。如果我们传入2和10,则会得到第11~20个对象,以此类推。

pagination方法跟first()和all()方法有不同之处,因为它返回的是一个pagination对象,而不是数据模型对象的列表。比如,我们想得到前10个(虚构的)Post对象,并将其显示在博客的第1页上:

>>> Post.query.paginate(1,10)
<flask_sqlalchemy.Pagination object at 0x0000000004490908>

这个对象有几个有用的属性:

>>> page = User.query.paginate(1,10)

#返回这一页包含的数据对象:

>>> page.items
[<User `fake_name`>, <User `zhangxa`>]

#返回这一页的页数

>>> page.page
1

#返回总页数

>>> page.pages
1

#上一页和下一页是否有对象可以显示

>>> page.has_prev,page.has_next
(False, False)

#返回上一页和下一页的pagination对象

#如果不存在的话则返回当前而

>>> page.prev(),page.next()
(<flask_sqlalchemy.Pagination object at 0x0000000004490908>, <flask_sqlalchemy.P
agination object at 0x0000000003360DA0>)

条件查询

现在我们来看SQL最擅长的事情,根据一些条件的集合获得过滤后的数据。要得到满足一系列等式条件的数据列表,则我们可以使用query.filter_by过滤器。query.filter_by过滤器接收关键字参数,并把接收到的参数作为我们想要在数据库里查询的字段名值对。比如,要得到用户名为fake_name的用户列表,则可以这样:

>>> users = User.query.filter_by(username='fake_name').all()
>>> users
[<User `fake_name`>]

这个例子只基于一个值进行过滤,但filter_by过滤器也可以接收多个值进行过滤。跟我们之前的函数类似,filter_by也是可以链式调用的。

>>> users = User.query.order_by(User.username.desc()).filter_by(username='fake_name').limit(2).all()
>>> users
[<User `fake_name`>]

query.filter_by只有在你确切地知道要查询的值时,才能够工作。使用query.filter则可以避免这一不便之处,你可以把一个比较大小的Python表达式传给它:

>>> user = User.query.filter(User.id > 1).all()
>>> user
[<User `zhangxa`>]

这只是个简单的例子,实际上query.filter可以接收任何Python的比较表达式。对于Python的常规类型,比如整数,字符串和日期,你可以使用==操作符来表示相等的比较。对于类型为整数,浮点或者日期的列,还可以用>,<,<=和>=操作符来表示不等的比较。

另外,一些复杂的SQL查询也可以转为用SQLAlchemy的函数来表示。例如,可以像下面这样实现SQL中IN,OR和NOT的比较操作。

>>> from sqlalchemy.sql.expression import not_, or_
>>> user = User.query.filter(User.username.in_(['fake_name']),User.password==Non
e).first()
>>> user
<User `fake_name`>

#找出拥有密码的用户

<User `fake_name`>
>>> user = User.query.filter(not_(User.password == None)).first()
>>> user

#这些方法都可以被组合起来

>>> user = User.query.filter(or_(not_(User.password == None),User.id>=1)).first()
>>> user
<User `fake_name`>

在SQLAlchemy中,与None的比较会被翻译成与NULL的比较。

修改数据

在使用first()或者all()等方法返回数据之前,调用update方法可以修改已存在的数据的值。

>>> User.query.filter_by(username='fake_name').update({'password':'test'})
1

#对数据模型的修改已被自动加入session中

>>> db.session.commit()
>>> user = User.query.filter_by(username='fake_name')
>>> user
<flask_sqlalchemy.BaseQuery object at 0x00000000044C95F8>
>>> user[0]
<User `fake_name`>
>>> user[0].password
'test'

删除数据

如果我们要从数据库中删除一行数据,则可以:

>>> user = User.query.filter_by(username='fake_name').first()
>>> db.session.delete(user)
>>> db.session.commit()

数据模型之间的关联

数据模型之间的关联在SQLAlchemy里表现为两个或者更多模型之间的链接,模型之间可以互相建立引用。这使得相关联的数据能够很容易地从数据库中取出,例如文章和它的评论,这就是关系数据库管理系统(RDBMS)中"关系“的含义,它给这类数据库带来了强大的功能。

现在让我们来创建第1个关联关系。在我们的博客网站上会有一些博客文章,每篇文章都有一个特定的作者。通过把每个作者的文章跟这个作者建立关联,可以方便地获取这个作者的所有文章,这显然是合理的做法。这就是一对多关系的一个范例。

一对多

我们先建立一个数据模型,用来表示网站上的博客文章:

class Post(db.Model):
    id = db.Column(db.Integer(), primary_key=True)
    title = db.Column(db.String(255))
    text = db.Column(db.Text())
    publish_date = db.Column(db.DateTime())
    user_id = db.Column(db.Integer(), db.ForeignKey('user.id'))

    def __init__(self, title):
        self.title = title

    def __repr__(self):
        return "<Post `{}`>".format(self.title)
注意user_id字段,对于关系数据库熟悉的朋友立刻会明白,它表示了一个外键约束(Foreign Key Constraint)。外键约束是数据库中的一种约束规则,在这里,它强制要求user_id字段的值存在于user表的id列中。这是数据库进行的一项检查,用来保证每个Post对象都会对应到一个已有的user。传给db.ForeignKey的参数,是一个用来代表user表的id列的字符串。如果你要用__tablename__自定义表名,则需要同时修改这个字符串。之所以直接用表名,而不是使用User.id引用,是因为在SQLAlchemy初始化期间,User对象可能还没有被创建出来。

user_id字段还不足以让SQLAlchemy建立我们想要的关联,我们还需要这样修改User对象:

class User(db.Model):
    id = db.Column(db.Integer(), primary_key=True)
    username = db.Column(db.String(255))
    password = db.Column(db.String(255))
    posts = db.relationship(
        'Post',
        backref='user',
        lazy='dynamic'
    )
    
    def __init__(self, username):
        self.username = username

    def __repr__(self):
        return "<User `{}`>".format(self.username)
db.relationship函数在SQLAlchemy中创建了一个虚拟的列,它会和我们的Post对象中的db.ForeignKey建立联系。待会儿我们再来讲backref的含义,不过lazy参数又是什么?lazy参数会告诉SQLAlchemy如何去加载我们指定的关联对象。如果设为子查询方式,则会在加载完User对象的时候,就立即加载与其关联的对象。这样会让总查询数量减少,但如果返回的条目数量很多,就会比较慢。另外,也可以设置为动态方式,这样关联对象会在被使用的时候再进行加载,并且在返回前进行过滤。如果返回的对象数很多,或者未来会很多,那最好采用这种方式。
我们现在就可以使用User.posts属性来得到一个posts列表,其中每项的user_id值都跟我们的User.id值相等。下面可以在命令行里试一下:

>>> user = User.query.get(1)
>>> new_post = Post('Post Title')
>>> new_post.user_id = user.id
>>>
>>> user.posts
<sqlalchemy.orm.dynamic.AppenderBaseQuery object at 0x0000000001039F98>
>>> db.session.add(new_post)
>>> db.session.commit()
>>> user.posts
<sqlalchemy.orm.dynamic.AppenderBaseQuery object at 0x0000000004427BA8>
>>> post = Post.query.get(1)
>>> post
<Post `Post Title`>
>>>

backref参数则可以使我们通过Post.user属性对User对象进行读取和修改。例如:

>>> second_post = Post('Second Title')
>>> second_post.user = user
>>> db.session.add(second_post)
>>> db.session.commit()
>>> user.posts
<sqlalchemy.orm.dynamic.AppenderBaseQuery object at 0x000000000435E9B0>
>>> user.posts[0]
<Post `Post Title`>
>>> user.posts[1]
<Post `Second Title`>
>>>

由于user.posts是一个列表,所以我们也可以通过把Post对象直接添加进这个列表,来自动保存它:

>>> second_post = Post('Second Ttile')
>>> user.posts.append(second_post)
>>> db.session.add(user)
>>> db.session.commit()
>>> list(user.posts)
[<Post `Post Title`>, <Post `Second Title`>, <Post `Second Ttile`>]

由于backref选项被设置为动态方式,所以我们既可以把这个关联字段看作列表,也可以把它看作一个查询对象:

>>> user.posts
<sqlalchemy.orm.dynamic.AppenderBaseQuery object at 0x0000000004427208>
>>> user.posts.order_by(Post.publish_date.desc()).all()
[<Post `Post Title`>, <Post `Second Title`>, <Post `Second Ttile`>]
>>>

在开始学习下一种关联类型之前,我们再创建一个数据模型,用来实现用户评论,并加上一对多的关联,稍后将会用到:

class Post(db.Model):
    id = db.Column(db.Integer(), primary_key=True)
    title = db.Column(db.String(255))
    text = db.Column(db.Text())
    publish_date = db.Column(db.DateTime())
    comments = db.relationship(
        'Comment',
        backref='post',
        lazy='dynamic'
    )
    user_id = db.Column(db.Integer(), db.ForeignKey('user.id'))

    def __init__(self, title):
        self.title = title

    def __repr__(self):
        return "<Post `{}`>".format(self.title)

class Comment(db.Model):
    id = db.Column(db.Integer(), primary_key=True)
    name = db.Column(db.String(255))
    text = db.Column(db.Text())
    date = db.Column(db.DateTime())
    post_id = db.Column(db.Integer(), db.ForeignKey('post_id'))
    
    def __repr__(self):
        return "<Comment `{}`>".format(self.text[:15])

多对多

如果我们有两个数据模型,它们不但可以相互引用,而且其中的每个对象都可以引用多个对应的对象,那应该怎么做呢?比如,我们的博客文章需要加上标签,这样用户就能轻松地把相似的文章分组。每个标签都对应了多篇文章,而每篇文章同时对应了多个标签。这样的关联方式叫作多对多的关联。考虑如下的例子:

tags = db.Table('post_tags',
                db.Column('post_id', db.Integer, db.ForeignKey('post.id')),
                db.Column('tag_id', db.Integer, db.ForeignKey('tag.id'))
                )

class Post(db.Model):
    id = db.Column(db.Integer(), primary_key=True)
    title = db.Column(db.String(255))
    text = db.Column(db.Text())
    publish_date = db.Column(db.DateTime())
    comments = db.relationship(
        'Comment',
        backref='post',
        lazy='dynamic'
    )
    user_id = db.Column(db.Integer(), db.ForeignKey('user.id'))
    tags = db.relationship(
        'Tag',
        secondary=tags,
        backref=db.backref('posts',lazy='dynamic')
    )
    
    def __init__(self, title):
        self.title = title

    def __repr__(self):
        return "<Post `{}`>".format(self.title)

class Tag(db.Model):
    id = db.Column(db.Integer(), primary_key=True)
    title = db.Column(db.String(255))
    
    def __init__(self, title):
        self.title = title

    def __repr__(self):
        return "<Tag `{}`>".format(self.title)

db.Table对象对数据库的操作比db.Model更底层。db.Model是基于db.Table提供的一种对象化包装方式,用来表示数据库表里的某行记录。这里之所以使用db.Table,正是因为我们不需要专门读取这个表的某行记录。
我们用tags变量来代表post_tags表,这个表有两个字段:一个表示博客文章的id,另一个表示某个标签的id。下面的例子演示了这种用法,如果表中有如下数据:

post_id                    tag_id

1                                  1

1                                  3

2                                  3

2                                  4

2                                  5

3                                  1

3                                  2

则SQLAlchemy会将其翻译成:

id为1的文章拥有id为1和3的标签。
id为2的文章拥有id为3,4和5的标签。
id为3的文章拥有id为1和2的标签。
你可以把这组数据简单地理解为标签和文章的关联关系。

在上面的程序中我们又使用了db.relationship函数来设置所需的关联,但这次多传了一个secondary(次级)参数,secondary参数会告知SQLAlchemy该关联被保存在tags表里。让我们在下面的代码中体会一下这种用法:

>>> post_one = Post.query.filter_by(title='Post Title').first()
>>> post_two = Post.query.filter_by(title='Second Title').first()
>>> tag_one = Tag('Python')
>>> tag_two = Tag('SQLAlchemy')
>>> tag_three = Tag('Flask')

>>> post_one.tags = [tag_two]
>>> tag_two
<Tag `SQLAlchemy`>
>>> tag_one
<Tag `Python`>
>>> post_one
<Post `Post Title`>
>>> post_two
<Post `Second Title`>
>>> tag_one
<Tag `Python`>
>>> post_two.tags = [tag_one,tag_two,tag_three]
>>> tag_two.posts
<sqlalchemy.orm.dynamic.AppenderBaseQuery object at 0x00000000044B2BA8>
>>> list(tag_two.posts)
[<Post `Post Title`>, <Post `Second Title`>]
>>> db.session.add(post_one)
>>> db.session.add(post_two)
>>> db.session.commit()

在设置一对多的关联时,主关联字段实际上是一个列表。现在主要的不同之处在于,backref也变成了一个列表。由于它是一个列表,所以我们也可以像这样把文章添加到标签里:

>>> tag_one.posts.append(post_one)
>>> post_one.tags
[<Tag `SQLAlchemy`>, <Tag `Python`>]
>>> db.session.add(tag_one)
>>> db.session.commit()
>>>

SQLAlchemy会话对象的方便之处

现在你了解了SQLAlchemy的好处,也就应该能了解SQLAlchemy的会话对象是什么,以及为什么开发网络应用少不了它们。如之前所说,会话可以被简单地描述为用来跟踪数据模型变化的对象,它还可以根据我们的指令将这些变化提交进数据库。不过,它的作用远不止这些。

首先,会话可以用来控制事务。事务是一组变更集,在提交的时候被一起写入数据库。

事务提供了很多看不见的功能。首先,当对象之间有关联的时候,事务会自动决定保存的先后顺序。在上一节我们保存标签的时候你可能已经注意到了这一点,当我们把新标签关联到文章的时候,会话对象会自动先把标签保存起来,尽管我们没有专门告诉它要提交标签对象。如果我们直接使用底层的数据库连接和SQL查询进行开发,就必须格外小心,对于哪些记录跟哪些记录有关联,需要自己记录下来,以避免在保存外键时指向了不存在的对象。

事务还会在数据库发生变更的时候,将当前数据标记为旧数据,当我们下次读取这项数据的时候,它就会先向数据库发送一条查询,以更新当前数据。这些工作都是在后台自动进行的。如果没有使用SQLAlchemy,则我们必须手工记录哪些数据行需要被更新,并且只更新那些必须更新的数据行,以高效地使用数据库资源。

其次,事务会避免出现两个不同的引用指向数据库中同一行记录的情况。这都归功于查询是在会话中进行的(Model.query实际上是db.session.query(Model)),如果事务中的一个数据行已经被查询过,则会直接返回指向这个数据对象的引用,而不会创建一个新的对象。如果没有这样的检查,则可能会出现两个表示同一行数据的不同对象,分别把不同的修改提交到数据库,这会造成很难发现和捕捉的隐性问题。

要注意,Flask SQLAlchemy会为每一个request创建一个新的会话对象,在request处理结束的时候,会丢弃没有提交的所有更改。因此一定要记得把工作保存下来。

使用Alembic进行数据库迁移

一个网络应用的功能总会不断地发生改变,增加新功能的时候,我们通常需要修改数据库结构。不论你是增删字段还是创建新表,数据模型的修改会贯穿你的应用开发的始终。但是,当数据库更改变得频繁后,你会很快面临一个问题:当把这些更改从开发环境迁移到生产环境时,如果不人工对数据模型和对应表的每一行修改进行仔细比较,那么你怎样才能保证所有的更改都会被迁移过去?又比如,要是你想要把开发环境的代码回滚到Git中的某个历史版本,用来尝试复现目前的生产环境中该版本代码出现的某个问题,那么你应该怎样把你的数据库结构调整到该版本对应的状态,而无须大量的额外工作呢?

作为程序员,我们痛恨除开发外的额外工作。还好有个工具可以解决这个问题,这个工具是Alembic,可以根据我们的SQLAlchemy模型的变化,自动创建数据库迁移记录。数据库迁移记录(Database migration)保存了我们的数据库结构变化的历史信息。Alembic让我们可以把数据库升级或者降级到某个已保存的特定版本,而跨越好几个版本之间的升级或者降级,则会执行这两个选定版本之间的所有历史记录文件。Alembic最棒的地方在于,这些历史文件本身就是Python程序文件。下面我们创建第1个数据库迁移记录,你会发现Alembic的语法非常简单。

注:Alembic不会捕捉所有可能的变更,比如,它不会记录SQL索引的变化。读者在每次迁移记录后,应该去检查一下迁移记录文件,并进行必要的修正。

我们不会直接使用Alembic,而是会使用Flask-Migrate,这是为SQLAlchemy专门创建的一个扩展,并且可以跟Flask Script一起使用。下面在pip中进行安装:

root@ubuntu:/home/zhangxa# pip3 install Flask-Migrate

在使用前需要把命令加到manage.py文件:


from flask_script import Manager, Server
from flask_migrate import Migrate, MigrateCommand

from main import app, db, User, Post, Tag, Comment

migrate = Migrate(app, db)

manager = Manager(app)
manager.add_command("server", Server())
manager.add_command("db", MigrateCommand)

@manager.shell
def make_shell_context():
    return dict(app=app, db=db, User=User, Post=Post, Tag=Tag, Comment=Comment)

if __name__ == "__main__":
    manager.run()

我们通过app对象和SQLAlchemy的实例初始化了Migrate对象,然后让迁移命令可以通过mange.py db来调用。运行下面的命令可以看到可用命令列表:

Z:\python\flask_tutorials>python manage.py db

usage: Perform database migrations

Perform database migrations

positional arguments:
  {downgrade,init,heads,history,upgrade,current,show,merge,revision,stamp,migrat
e,branches,edit}
    downgrade           Revert to a previous version
    init                Creates a new migration repository
    heads               Show current available heads in the script directory
    history             List changeset scripts in chronological order.
    upgrade             Upgrade to a later version
    current             Display the current revision for each database.
    show                Show the revision denoted by the given symbol.
    merge               Merge two revisions together. Creates a new migration
                        file
    revision            Create a new revision file.
    stamp               'stamp' the revision table with the given revision;
                        don't run any migrations
    migrate             Alias for 'revision --autogenerate'
    branches            Show current branch points
    edit                Edit current revision.

optional arguments:
  -?, --help            show this help message and exit

要开始跟踪我们的数据库变更,则可使用init命令

Z:\python\flask_tutorials>python manage.py db init

这会在项目目录中创建一个叫作migrations的文件夹,所有的记录文件会被保存在里面。现在我们可以开始进行首次迁移:

Z:\python\flask_tutorials>python manage.py db migrate -m "initial migration"

这个命令会让Alembic扫描我们所有的SQLAlchemy对象,找到在此之前没有被记录过的所有表和列,由于这是第1次提交,所以迁移记录文件会比较大。确保使用了-m参数来保存提交信息,通过提交信息寻找所需的迁移记录版本是最容易的办法。每个迁移记录文件都被保存在migrations/versions/文件夹中。

执行下面的命令,就可以把迁移记录应用到数据库上,并改变数据库的结构:

Z:\python\flask_tutorials>python manage.py db upgrade

要返回以前的版本,则可以根据histroy命令找到版本号,然后传给downgrade命令:

Z:\python\flask_tutorials>python manage.py db history

Z:\python\flask_tutorials>python manage.py db downgrade xxxx

同Git一样,每个迁移记录都由一个哈希值来表示。这是Alembic的重要功能,但只用于它的表层。你也可以尝试把迁移记录和你的Git提交记录对应起来,这样当你把代码回滚到Git中的某个版本时,也能很容易地升级或降级数据库结构。

总结

现在我们已经能轻松地操纵数据了,接下来可以在应用中显示这些数据。下一节会告诉你如何基于数据模型动态地创建HTML,以及如何通过网页来添加新数据。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值