慕课网Flask高级编程实战-9.书籍交易模型(数据库事务、重写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)

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏java学习

Java基础第一天学习笔记

01.01_计算机基础知识(计算机概述)(了解) * A:什么是计算机?计算机在生活中的应用举例 * 计算机(Computer)全称:电子计算机,俗称电脑。是...

37950
来自专栏.NET开发者社区

什么是ORM?为什么用ORM?浅析ORM的使用及利弊

什么是ORM ORM(Object-relational mapping),中文翻译为对象关系映射,是一种为了解决面向对象与关系数据库存在的互不匹配的现象的技术...

222100
来自专栏电光石火

PHP中HTTP防盗链技术

 盗链是指服务提供商自己不提供服务的内容,通过技术手段绕过其它有利益的最终用户界面(如广告),直接在自己的网站上向最终用户提供其它服务提供商的服务内容,骗取最终...

23080
来自专栏听雨堂

根据rpt文件打印报表

不用一个个地导入rpt文件,再去写固定的代码,而是每次动态的根据指定的名称去加载报表和打印   if(Session["PrintXml"]!=null)   ...

257100
来自专栏java学习

学习java需要会哪些知识才能够去应聘工作?

按照我去培训机构的学习经历,给初学还有自学Java 的同学一个基本的学习脉络,希望对大家有帮助。 不建议找到一本书死啃,没啥用,不要有这一页看不明白我就不往下看...

338100
来自专栏电光石火

PHP中HTTP防盗链技术

 盗链是指服务提供商自己不提供服务的内容,通过技术手段绕过其它有利益的最终用户界面(如广告),直接在自己的网站上向最终用户提供其它服务提供商的服务内容,骗取...

33350
来自专栏Golang语言社区

channel机理及调度理解

《Go语言编程》一书介绍了libtask库,可以认为这个库等同于go的底层goroutine实现。

11730
来自专栏deed博客

day01笔记

17850
来自专栏王清培的专栏

WebAPi的可视化输出模式(RabbitMQ、消息补偿相关)——所有webapi似乎都缺失的一个功能

最近的工作我在做一个有关于消息发送和接受封装工作。大概流程是这样的,消息中间件是采用rabbitmq,为了保证消息的绝对无丢失,我们需要在发送和接受前对消息进行...

26190
来自专栏walterlv - 吕毅的博客

不再为命名而苦恼!使用 MSTestEnhancer 单元测试扩展,写契约就够了

发布于 2018-02-22 11:52 更新于 2018-08...

15210

扫码关注云+社区

领取腾讯云代金券