一、鱼豆
1.1 建立索要此书对应的wish模型
赠送书籍的行为,被抽象成为gift;那么,索要书籍的行为,也可以被抽象成wish。
- 新建目录:
- 在
app|models|wish.py
中:
跟gift模型的属性基本相同
from sqlalchemy import Column, Integer, Boolean, ForeignKey, String
from sqlalchemy.orm import relationship
from app.models.base import Base
class Wish(Base):
id = Column(Integer, primary_key=True) # 礼物的唯一标识
launched = Column(Boolean, default=False) # 礼物有没有送出去?默认False,礼物没有赠送出去
user = relationship('User') # 在Wish模型中,引用User这个模型,用relationship表示引用关系
uid = Column(Integer, ForeignKey('user.id')) # 具体是引用哪一个用户呢? # 外键ForeignKey:数据库术语
isbn = Column(String(15), nullable=False) # 同一本书可以被赠送多次 # 在gift模型中,引用book这个模型
不管是赠送此书,还是索要此书,都有一个前提就是必须先确定用户的身份,即用户应该已经注册和登录完成。
注册和登录,恰好已经在上一章节搞定。万事俱备。
1.2 开始编写赠送此书的业务逻辑
在app|web|gift.py
中:
赠送此书的关键:要将关于该业务逻辑的两个变量放进去:谁赠送的书?赠送的哪本书?
from . import web
from flask_login import login_required, current_user # 代表当前访问网站的用户
from ..models.base import db
from ..models.gift import Gift
@web.route('/gifts/book/<isbn>')
@login_required
def save_to_gifts(isbn):
'''
赠送此书
'''
gift = Gift()
gift.isbn = isbn # 在保存之前,要将关于该业务逻辑的变量放进去:谁赠送的书?赠送的哪本书?
gift.uid = current_user.id # current_user:其实是实例化后的User的模型
db.session.add(gift) # 实例化一个对象,并将对象保存起来
db.session.commit()
1.3 鱼豆:每赠送一本书,系统会赠送0.5个鱼豆
下图代表的,其实是一套经济系统。
在app|web|gift.py
中:
编写每赠送一本书,系统会赠送0.5个鱼豆的业务逻辑
from . import web
from flask_login import login_required, current_user # 代表当前访问网站的用户
from ..models.base import db
from ..models.gift import Gift
@web.route('/gifts/book/<isbn>')
@login_required
def save_to_gifts(isbn):
'''
赠送此书
'''
gift = Gift()
gift.isbn = isbn # 在保存之前,要将关于该业务逻辑的变量放进去:谁赠送的书?赠送的哪本书?
gift.uid = current_user.id # current_user:其实是实例化后的User的模型
current_user.beans += current_app.config['BEANS_UPLOAD_ONE_BOOK'] # 鱼豆的属性,在User模型中早已定义
db.session.add(gift) # 实例化一个对象,并将对象保存起来
db.session.commit()
在app|setting.py
中:
PER_PAGE = 15
BEANS_UPLOAD_ONE_BOOK = 0.5 # 0.5是会变化的,应该放在本配置文件中。然后被上面调用
二、思维逻辑锻炼:补写四个逻辑漏洞
2.1 四个逻辑漏洞
上小节的代码存在如下四个问题:
(1)isbn没有验证的两种情况。
验证是否是13位或10位数字形式的标准isbn?鱼书api中是否有该isbn对应的图书?
(2)缺少两种细节校验
一本书,已经作为礼物放进了赠送清单中,此时是不允许再次添加这本书到赠送清单的。
一个用户,不可能既是赠书人,又是要书人。
即,上面两个可合并为一个条件:一本书必须既不在赠送清单中,也不在心愿清单中,才能被用户添加操作。
2.2 代码补写
将验证是否是ISBN编号,视为用户的行为。
- 在
app|models|user.py
中
from sqlalchemy import Column, Integer, String, Boolean, Float
from werkzeug.security import generate_password_hash, check_password_hash
from app.libs.helper import is_isbn_or_key
from app.models.base import db, Base
from flask_login import UserMixin
from app import login_manager
from app.models.gift import Gift
from app.models.wish import Wish
from app.spider.yushu_book import YuShuBook
class User(Base, UserMixin):
id = Column(Integer, primary_key=True) # 用户的唯一标识
nickname = Column(String(24), nullable=False)
phone_number = Column(String(18), unique=True)
_password = Column('password', String(128), nullable=False)
email = Column(String(50), unique=True, nullable=False)
confirmed = Column(Boolean, default=False)
beans = Column(Float, default=0)
send_counter = Column(Integer, default=0)
receive_counter = Column(Integer, default=0)
wx_open_id = Column(String(50))
wx_name = Column(String(32))
def can_save_to_list(self, isbn):
'''验证是否是ISBN编号,返回的永远是bool值'''
if is_isbn_or_key(isbn) != 'isbn': # 如果不是13位或10位数字形式的标准isbn,就不允许保存这样的书籍
return False
# 如果是13位或10位数字形式的标准isbn,那么执行下面语句
yushu_book = YuShuBook()
yushu_book.search_by_isbn(isbn) # 鱼书api中是否有该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() # 该书是否存在心愿清单中?
if not gifting and not wishing: # 一本书必须既不在赠送清单中,也不在心愿清单中,才能是True
return True
else:
return False
做web编程与做算法的对比
对比 | 做web编程 | 做算法 |
---|---|---|
难 | 业务需求千变万化,需要开发者具备较强的逻辑思维能力 | |
易 | 不太需要数学知识,也不太需要算法功底 | 学熟练之后,算法其实就是个条条框框、固定的套路 |
- 在视图函数
app|web|gift.py
中:
from flask import current_app, flash
from . import web
from flask_login import login_required, current_user # 代表当前访问网站的用户
from ..models.base import db
from ..models.gift import Gift
@web.route('/my/gifts')
@login_required # 装饰器的顺序很重要,必须在注册路由的下面
def my_gifts():
return 'My Gifts'
@web.route('/gifts/book/<isbn>')
@login_required
def save_to_gifts(isbn):
'''
赠送此书
'''
if current_user.can_save_to_list(isbn): # 调用刚刚编写好的函数 # 如果是True,允许添加到清单中:方可执行以下语句
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.commit()
else:
flash('这本书已添加至你的赠送清单或已存在于你的心愿清单,请不要重复添加')
can_save_to_list
函数的校验,写在了User
模型里,而没有写在forms
验证层中。
因为,如果看作参数验证,可以放进forms
验证层中;如果看作用户的行为,也可以放进User
模型里。
三、事务与回滚
3.1 天然支持保证数据一致性的事务
1.事务的概念
事务是一个数据库的概念,但是放在模型中也是存在的。
- 在视图函数
app|web|gift.py
中:
...
@web.route('/gifts/book/<isbn>')
@login_required
def save_to_gifts(isbn):
'''
赠送此书
'''
if current_user.can_save_to_list(isbn):
gift = Gift() # 操作是gift数据表
gift.isbn = isbn
gift.uid = current_user.id
~~~~~~~~~~~~~~~~~~~~~~~~~~
current_user.beans += current_app.config['BEANS_UPLOAD_ONE_BOOK'] # 操作的是user数据表
db.session.add(gift)
db.session.commit()
else:
flash('这本书已添加至你的赠送清单或已存在于你的心愿清单,请不要重复添加')
如果程序运行到波浪线处,突然程序中断了,current_user
中的鱼豆并没有加上,这会不会造成数据的异常。
为了保证数据的完整性和一致性,应该两张数据表同时操作,要么同时不操作。
这种保证数据一致性的方法,叫做事务。
2.如何在sqlalchemy
中进行事务的操作?
sqlalchemy
天然支持,且上面的代码已经支持了事务。
因为,其实波浪线前后的两张表,只有运行到db.session.commit()
时,才会同时提交。之前,都不会提交。
3.2 回滚rollback
上面代码仍存在的问题:没有执行数据库的回滚。
在视图函数app|web|gift.py
中:
...
@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.commit() # 提交
except Exception as e: # 如果在整个try语句内部出现错误,就要执行下面语句
db.session.rollback()
raise e # 把这个异常抛出去
else:
flash('这本书已添加至你的赠送清单或已存在于你的心愿清单,请不要重复添加')
如果程序执行到db.session.commit()
这里提交的时候出现错误,而又没有执行回滚roolback的话,那么本次和后续的sqlalchemy
的插入操作也是会失败的。
因此,建议只要使用db.session.commit()
,就都要用try except
将其包裹起来,然后在except
后面执行下db.session.rollback()
回滚一下。
还是没明白没啥必须回滚?
四、解决因写回滚和抛异常多处代码重复问题
问题:
在整个项目中,执行db.session.commit()
提交数据库的操作其实是很多的,也会一直重复写后面的回滚和抛出异常代码。如何避免写上面的重复代码?
4.1 上下文管理器@contextmanager
装饰器的两种定义方式
1.传统的定义方式:
必须在类里面定义__enter()_
与__exit__()
这两个方法。
class MyResourse: # 类是一个上下文管理器,因为它里面实现了`__enter()_`与`__exit__()`这两个方法
def __enter__(self):
print('connect to resourse') # 连接资源
return self
def __exit__(self, exc_type, exc_value, tb):
print('close resourse connection')
def query(self): # 定义一个业务方法
print('query data')
with MyResourse() as r: # 用with来应用上下文管理器 # 将类的实例化对象,作为上下文表达式
r.query()
2.简化定义的方法:
不用在类里定义__enter()_
与__exit__()
这两个方法,而是利用contextlib
模块下的contextmanager
装饰器
from contextlib import contextmanager
class MyResourse: # 该类只是一个普通的类,已不是上下文管理器了
def query(self): # 定义一个业务方法
print('query data')
@contextmanager
def make_myresaourse():
print('connect to resourse') # 其实是__enter__()方法里的逻辑
return MyResourse() # 其实是__enter__()方法里的逻辑
print('close resourse connection') # 其实是__exit__()方法里的逻辑
with make_myresaourse() as r:
r.query()
上面是有问题的,因为一旦整个函数return之后,后面的代码是不会执行的。这与上下文管理器期望的执行顺序是不一致的。
改正如下:
用yield
关键字替换return
。就可保证执行顺序是:⑴-->⑵-->⑶ -->⑷
from contextlib import contextmanager
class MyResourse: # 只是一个普通的类,已不是上下文管理器了
def query(self): # 定义一个业务方法
print('query data')
@contextmanager
def make_myresaourse():
print('connect to resourse') # 其实是__enter__()方法里的逻辑 ⑴
yield MyResourse() # 其实是__enter__()方法里的逻辑 ⑵
print('close resourse connection') # 其实是__exit__()方法里的逻辑 ⑷
with make_myresaourse() as r: # 第一步
r.query() # 第二步 ⑶
-->
connect to resourse
query data
close resourse connection
以上带有yield
关键字的函数,叫做生成器。叫什么名字不重要,重要的是知道yield
关键字与return
关键字的区别。
3.反思:
利用contextmanager
装饰器,真的简化了上下文管理器的定义吗?
其实,不但没有简化,反而复杂了,理解也更抽象了。
但,这种写法确实有一些好处。
4.2 用打印书名小案例,揭示@contextmanager
的另外功能
1.原来打印一本书的名字
print('且将生活一饮而尽')
-->
且将生活一饮而尽
2.现在,在数据库中存储的是上面这种且将生活一饮而尽
,但是显示的时候想加个书名号。有没有自动加上书名号的办法?
利用contextmanager
装饰器。
from contextlib import contextmanager # 导入装饰器
@contextmanager
def book_mark():
print('《') # 打印书名号的前半部分
yield # yield不需要返回任何结果,因为下面的with语句中没有 as
print('》') # 打印书名号的后半部分
pass
with book_mark(): # 用with语句,调用上面刚定义好的函数
print('且将生活一饮而尽')
-->
《
且将生活一饮而尽
》
格式上默认自动换行了,利用小技巧end=
修正下格式:
from contextlib import contextmanager
@contextmanager
def book_mark():
print('《', end='')
yield
print('》', end='')
pass
with book_mark():
print('且将生活一饮而尽', end='')
-->
《且将生活一饮而尽》
知识无所谓简单、复杂,只要能解决问题就好的知识。
由上可知,contextmanager
还有另外的用法:
这与上下文管理器完全没有关系,纯粹是想在要执行的核心代码前面和后面,各补充执行一段代码。
4.3 结合解决代码重复问题
第二种方法,即利用@contextmanager
定义上下文管理器的方法,其实给给了我们一个机会,就是把不是上下文管理器的类,变成了一个上下文管理器。
假如,MyResourse
类是自己编写的,确实可以在其内部定义__enter()_
与__exit__()
两个方法,用第一种方法。
但是,如果MyResourse
类是flask提供、或第三方类库提供给我们的,你去源码中修改加上__enter()_
与__exit__()
两个方法,合适吗?明显不合适。但是,我却可以在MyResourse
类的外部,将其包装成一个上下文管理器。
1.db
是sqlalchemy
,如何在一个第三方的类库中,为它新增加一个方法呢?
继承。子类不好命名,就修正父类的名字
在app|models|base.py|
中:
from flask_sqlalchemy import SQLAlchemy as _SQLAlchemy # 既然子类不好命名,就把父类的名字修正下
class SQLAlchemy(_SQLAlchemy): # 自定义一个子类SQLAlchemy
@contextmanager # 戴上一个装饰器,让下面的方法变为上下文管理器
def auto_commit(self): # 在第三方的类库中,为它新增加一个方法,方便其他地方调用
try: # 该行,是书名号的前半部分
yield # 就像是中间枢纽
self.session.commit() # 以下的四行,是书名号的后半部分
except Exception as e:
db.session.rollback()
raise e
db = SQLAlchemy()
class Base(db.Model):
__abstract__ = True
...
2.调用已变为上下文管理器的方法,消除重复代码
- 在视图函数
app|web|gift.py
中调用下:
...
@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)
else:
flash('这本书已添加至你的赠送清单或已存在于你的心愿清单,请不要重复添加')
- 也在视图函数
app|web|auth.py
中调用:
...
@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)
4.4 两个学习建议
1.复杂知识、新语法:分离分步
遇到比较比较复杂的的知识,比如contextmanager
,一定要将问题单独的分离出来,在单独的文件中编写一段非常简单的代码。
因为业务越简单,越能够有一个清晰的头脑,去关注知识或原理本身。比如,老师在学习新知识、新语法,就是这样。
2.高级编程
高级编程并不是学越来越多、越来越复杂的语法,最关键的是,用你所学过的知识,写出更好的代码。
学透《python入门与进阶》的知识点足矣:根据多年经验,选取的正是python中最实用的部分。因为,python中的高级语法是学不完的,太多了。
所以,要养成知识综合应用的能力:单个知识点很好理解,但你能不能将其组合在一起,写出更好的代码来。
比如为解决上面因写回滚和抛异常多处代码重复的问题:
- 对生成器的理解
yield
- 对
session.commit
的理解 - 对
session.rollback
的理解 - 对面向对象继承的理解
培养知识综合应用能力最好的途径:看各种优秀框架的源码
五、类变量的陷阱
5.1 测试下赠送此书功能
打开搜索结果页面,测试下赠送此书这个功能:
在未登录的状态,访问http://localhost:81/book/search?q=村上春树
:
最终结果是报错,因为视图函数save_to_gifts()
没有return返回值。
但是,此时,数据表gift
中已经添加了刚才的要赠送的书籍信息。
5.2 谈下creat_time
的意义:
1.creat_time
用于记下该条记录的插入时间
上面四个表中的该属性均是空值。这是因为在基类模型中,并没有给属性creat_time
赋值。
如何赋值?应该赋一个什么样子的值?
2.在构造函数中对类变量进行赋值
在视图函数app|models|base.py
中:
在构造函数中对其进行赋值,将属性creat_time
指向实例化每个对象的时间,而不是放进类变量中赋值。
from datetime import datetime
class SQLAlchemy(_SQLAlchemy): # 自己定义一个子类
@contextmanager
def auto_commit(self): # 定义一个方法
try:
yield
self.session.commit()
except Exception as e:
db.session.rollback()
raise e
db = SQLAlchemy()
class Base(db.Model): # 让base是一个基类,,同时不去创建表
__abstract__ = True # 告诉SQL alchemy,我不想创建一个表数据
status = Column(SmallInteger, default=1) # 通过更改变量的状态,来控制这条数据是否被删除 # 1 表示此条数据存在
create_time = Column('create_time', Integer) # 类变量:记录该模型生成和保存的时间。此时,没有赋值。
def __init__(self):
self.create_time = int(datetime.now().timestamp()) # 将当前时间,转化为时间戳
def set_attrs(self, attrs_dict):
...
错误的写法:
调试模式下,creat_time
指向的是开启服务器的时间:因为类变量会让所有的对象,该属性都是同一个值。
Comments | NOTHING