本章是一个综合应用章节。我们将看到如何使用多个Python的知识点综合解决问题。我们将进一步的使用@contextmanager来改善前面所学到的上下文管理器,并结合yield来优化数据库事务。此外,我们还将重写Flask中的一些对象的方法,来实现我们自己的业务逻辑

9.1 鱼豆

我们的鱼书有一个经济系统,在上传一本书的时候,将获取0.5个鱼豆。赠送一个本书的时候,再获取1个鱼豆。索要一本书的时候,消耗一个鱼豆,其中赠送和索要书籍是用户之间鱼豆互相加减,上传的时候是系统赠送。

基于上面的规则,我们来编写赠送鱼书的视图函数。

1.判断当前书籍是否可以加入赠送清单

    1.如果isbn编号不符合规则,不允许添加
    2.如果isbn编号对应的书籍不存在,不允许添加
    3.同一个用户,不能同时赠送同一本书籍
    4.一个用户对于一本书不能既是赠书者,又是索要者
    5.3和4合并成一条,就是一本书必须即不在心愿清单又不在赠书列表里才可以添加

并不是web编程就简单,算法就难,他们都有自己难和简单的地方,对于web编程来说,他不需要算法,数学的支撑。但是需要很强的逻辑思维能力,因为业务一直在变化,需要非常好的抽象能力来适应变化。

models/user.py

    def can_save_to_list(self, isbn):
        """
        判断可以将书籍加入心愿清单
        1.如果isbn编号不符合规则,不允许添加
        2.如果isbn编号对应的书籍不存在,不允许添加
        3.同一个用户,不能同时赠送同一本书籍
        4.一个用户对于一本书不能既是赠书者,又是索要者
        5.3和4合并成一条,就是一本书必须即不在心愿清单又不在赠书列表里才可以添加
        :param isbn:
        :return:
        """
        if not is_isbn_or_key(isbn):
            return False

        yushu_book = YuShuBook()
        yushu_book.search_by_isbn(isbn)
        if not yushu_book.first:
            return False

        gifting = Gift.query.filter_by(uid=self.id, isbn=isbn, launched=False).first()
        wishing = Wish.query.filter_by(uid=self.id, isbn=isbn, launched=False).first()
        return not wishing and not gifting

之所以要把这个逻辑判断方法加在models里而不是在form里,是因为编程是活的,要视情况而定,这个can_save_to_list加载models在使用起来更加灵活,复用性更强。

2.添加赠送清单,增加鱼豆

添加赠送清单,增加鱼豆对应了两个数据库操作,如果其中一个在执行过程中失败了,那么另一个也不能提交,这用到了数据库的事务。 给用户添加鱼豆需要获取当前用户,我们可以从flask_login的current_user获取当前用户

@web.route('/gifts/book/<isbn>')
@login_required
def save_to_gifts(isbn):
    if current_user.can_save_to_list(isbn):
        try:
            gift = Gift()
            gift.isbn = isbn
            gift.uid = current_user.id

            current_user.beans += current_app.config['BEANS_UPLOAD_ONE_BOOK']

            db.session.add(gift)
            db.session.add(current_user)
            db.session.commit()
        except Exception as e:
            db.session.rollback()
            raise e
    else:
        flash("这本书以添加进您的赠送清单或已经存在于您的心愿清单,请不要重复添加")
    return redirect(url_for('web.book_detail', isbn=isbn))

3.添加心愿清单

web/wishs.py

@web.route('/wish/book/<isbn>')
@login_required
def save_to_wish(isbn):
    if current_user.can_save_to_list(isbn):
        with db.auto_commit():
            wish = Wish()
            wish.isbn = isbn
            wish.uid = current_user.id

            db.session.add(wish)
    else:
        flash("这本书以添加进您的赠送清单或已经存在于您的心愿清单,请不要重复添加")
    return redirect(url_for('web.book_detail', isbn=isbn))

4.巧用ajax

上面我们在添加赠送书籍完成之后,由重定向回了书籍详情页面。由于我们之前就是在数据详情页面,做了一次操作以后又重定向回去了,这样的操作时非常浪费服务器资源的。我们可以用ajax异步请求来改善这个问题。

另一个消耗服务器性能的点在于书籍详情页面的模板渲染工作,所以另一种优化方案,就是将页面作为一个静态页面缓存起来,下一次重定向只需要将缓存的页面读取出来返回即可

9.2 contextmanager

1.contextmanager简单讲解

contextmanager可以简化上下文管理器,不需要我们编写__enter__和__exit__函数。他给了我们一个机会,让我们把之前一个不是上下文管理器的类变成一个上下文管理器,而不需要我们去修改这个类的源代码

其中的yield的作用,是中断当前函数执行流程,先去执行yield出去的部分的代码执行流程

下面的代码的作用,在书籍前后自动加上《》

@contextmanager
def book_mark():
    print('《', end='')
    yield
    print('》', end='')


with book_mark():
    print('钢铁',end='')

2.结合继承,contextmanager,yield,rollback来简化try-except的数据库事务代码

1.我们可以通过contextmanager实现一个上下文管理器,将try-except的代码放在contextmanager里,将具体的业务逻辑代码yield出去 2.SQLAlchemy并没有这个上下文管理器,但是我们可以做一个子类,来扩展他的功能 3.编写子类的时候,命名是非常不好起的,我们可以改变父类的名字,给子类命名为原父类的名字

models/base.py

from flask_sqlalchemy import SQLAlchemy as _SQLAlcmemy

class SQLAlchemy(_SQLAlcmemy):
    @contextmanager
    def auto_commit(self):
        try:
            yield
            self.session.commit()
        except Exception as e:
            self.session.rollback()
            raise e

使用auto_commit的save_to_gifts视图函数

@web.route('/gifts/book/<isbn>')
@login_required
def save_to_gifts(isbn):
    if current_user.can_save_to_list(isbn):
        with db.auto_commit():
            gift = Gift()
            gift.isbn = isbn
            gift.uid = current_user.id

            current_user.beans += current_app.config['BEANS_UPLOAD_ONE_BOOK']

            db.session.add(gift)
            db.session.add(current_user)
    else:
        flash("这本书以添加进您的赠送清单或已经存在于您的心愿清单,请不要重复添加")
    return "aaa"

使用auto_commit的register视图函数

@web.route('/register', methods=['GET', 'POST'])
def register():
    form = RegisterForm(request.form)
    if request.method == 'POST' and form.validate():
        with db.auto_commit():
            user = User()
            user.set_attrs(form.data)

            db.session.add(user)

        return redirect(url_for('web.login'))

    return render_template('auth/register.html', form=form)

干货:

1.遇到比较复杂的问题,应该把他单独的分离出来,在一个单独的文件里来编写一些非常简单的源码,因为业务越简单,越能够让我们去关注知识和原理本身的相关问题。 2.高级编程不是在于学习更高级的语法(学会更好),更关键的在于能够用自己所学的知识,写出更好的代码来 3.对知识的综合运用能力很重要,将单个的知识点组合在一起写出一段很好的代码来

9.3 书籍交易视图模型

书籍详情页,除了需要显示书籍详情信息外。还应该显示其他信息,这些信息分为三类 1.默认情况下,显示想要赠送这本书的人的列表,包括名字和上传时间。 2.如果当前用户是此书的赠送者,应该显示索要这本书的人的列表。 3.如果当前用户是此书的索要者,应该显示想要赠送这本书的人的列表。

综上所述,我们一共需要两个列表,这本书的索要人列表和这本书的赠书人的列表,根据不同情况进行展示。

    # 赠书人列表和索要人列表
    trade_gifts = Gift.query.filter_by(isbn=isbn).all()
    trade_wishs = Wish.query.filter_by(isbn=isbn).all()

我们在view_model中处理这两个列表的原始数据,加工成我们姓名,上传时间的列表。由于gifts,wishs两个的加工逻辑一样,只是数据库表不一样,所以可以写一个统一的类trade来处理

class TradeInfo:

    def __init__(self, goods):
        self.total = 0
        self.trades = []
        self.__parse(goods)

    def __parse(self, goods):
        self.total = len(goods)
        self.trades = [self.__map_to_trade(single) for single in goods]

    def __map_to_trade(self, single):
        if single.create_datetime:
            time = single.create_datetime.strftime('%Y-%m-%d')
        else:
            time = '未知'
        return dict(
            user_name=single.user.nickname,
            time=time,
            id=single.id
        )

create_time 本是int类型,要进行strftime格式化操作需要转化成string类型,这个操作每个模型都要用到,所以编写在base.py里

    @property
    def create_datetime(self):
        if self.create_time:
            return str(self.create_time)
        else:
            return None

接下来完善书籍详情视图函数。区分上面说的三种情况。使用current_user的is_authenticated可以判断用户是否登录。然后分别以当前用户id为查询条件去wish表和gift表里查询,如果能查询到,则将对应的has_in_gifts/has_in_wishs设置为True

@web.route("/book/<isbn>/detail")
def book_detail(isbn):
    has_in_gifts = False
    has_in_wishs = False

    # 取出每本书的详情
    yushu_book = YuShuBook()
    yushu_book.search_by_isbn(isbn)
    book = BookViewModel(yushu_book.first)

    # 三种情况的判断
    if current_user.is_authenticated:
        if Gift.query.filter_by(uid=current_user.id).first():
            has_in_gifts = True
        if Wish.query.filter_by(uid=current_user.id).first():
            has_in_wishs = True

    # 赠书人列表和索要人列表
    trade_gifts = Gift.query.filter_by(isbn=isbn).all()
    trade_wishs = Wish.query.filter_by(isbn=isbn).all()
    return render_template("book_detail.html", book=book,
                           wishes=trade_wishs, gifts=trade_gifts,
                           has_in_wishs=has_in_wishs, has_in_gifts=has_in_gifts)

9.4 重写filter_by

由于我们的删除操作都是逻辑删除,所以在查询的时候应该默认查询status=1的记录(即未删除的记录),但是如果在每一个filter_by里都这么写,就太麻烦了,我们的思路是重写默认的filter_by函数,加上status=1的限制条件。

那么我们就需要先了解原来SQLAlchemy的继承关系 Flask的SQLAlchemy中有一个BaseQuery,BaseQuery继承了orm.Query(原SQLAlchemy的类),这里面有filter_by函数;也就是说BaseQuery通过继承orm.Query拥有了filter_by的能力

flask_sqlalchemy

...
...
class SQLAlchemy(object):
    Query = None

    def __init__(self, app=None, use_native_unicode=True, session_options=None,
                 metadata=None, query_class=BaseQuery, model_class=Model):
...
...

class BaseQuery(orm.Query):
...
...

orm.Query

    def filter_by(self, **kwargs):
        # for循环拼接关键字参数查询条件
        clauses = [_entity_descriptor(self._joinpoint_zero(), key) == value
                   for key, value in kwargs.items()]
        return self.filter(sql.and_(*clauses))

所以如果我们要重写filter_by,需要自己编写子类,继承BaseQuery,重写filter_by函数,将status=1加入到kwargs

class Query(BaseQuery):

    def filter_by(self, **kwargs):
        if 'status' not in kwargs:
            kwargs['status'] = 1
        return super(Query, self).filter_by(**kwargs)

最后,Flask的SQLAlchemy给了我们一种方法,让我们应用自己的Query类,即在实例化的时候传入关键字参数query_class

db = SQLAlchemy(query_class=Query)