9.书籍交易模型(数据库事务、重写flask中的对象)

一.鱼豆

新增app/web/gift.py:

from .blueprint import web
from flask_login import login_required, current_user
from app.models.gift import Gift
from app.models.base import db

__author__ = 'cannon'


@web.route('/my/gifts')
@login_required
def my_gifts():
    return 'my gifts'


@web.route('/gifts/book/')
@login_required
def save_to_gifts(isbn):
    gift = Gift()
    gift.isbn = isbn
    gift.uid = current_user.id   # current_user就是实例化后的user模型, 原因在于models/userget_user
    db.session.add(gift)
    db.session.commit()

鱼豆的逻辑:

9.书籍交易模型(数据库事务、重写flask中的对象)_第1张图片

修改save_to_gifts函数(先在setting.py配置文件中增加BEANS_UPLOAD_ONE_BOOK=0.5的每次上传书籍系统赠送鱼豆个数):

@web.route('/gifts/book/')
@login_required
def save_to_gifts(isbn):
    gift = Gift()
    gift.isbn = isbn
    gift.uid = current_user.id   # current_user就是实例化后的user模型, 原因在于models/userget_user
    current_user.beans +=  current_app.config['BEANS_UPLOAD_ONE_BOOK']   # 增加0.5个鱼豆
    db.session.add(gift)
    db.session.commit()

save_to_gift函数逻辑非常不严谨, 还有很多问题要处理


二. 逻辑思维

1.是否满足isbn规范
2.不允许一个用户同时赠送多本相同的书
3.一个用户不可能同时成为赠送者和索要者

在model/user.py的class User(UserMixin, Base): 中增加一个方法:


    def can_save_to_list(self, isbn):
        if is_isbn_or_key(isbn) != 'isbn':   # 是否满足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() # launched表示 如果有书但没送出去,你也不能上传书
        wishing = Wish.query.filter_by(uid=self.id, isbn=isbn, launched=False).first()
        if not gifting and not wishing:
            return True
        else:
            return False

其实代码中giftingwishing还有很大的坑filter_by,等我们去填。


三. 事务与回滚

在save_to_gifts使用can_save_to_list:

@web.route('/gifts/book/')
@login_required
def save_to_gifts(isbn):
    if current_user.can_save_to_list(isbn):  
    #调用在model中写的校验,而没有去form 写校验。
    #这里是用户的行为, model里校验用起来方便。form的校验复用性差
        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('这本书已添加至你的赠送清单或已存在于你的心愿清单,请不要重复添加!')
     return redirect(url_for('web.book.detail', isbn=isbn))

假如:程序执行到gift.uid = current_user.id突然中断了, 鱼豆并没有加上。这会导致数据的异常。

所以我们必须保证,这里gift和user两个表, 要么同时操作,要么都不操作。
在数据库中保证数据一致性的方法称为:事务。
其实sqlalchemy已经有相关机制了(我们代码中也写了):

db.session.add(gift)
db.session.commit()

只有执行了这代码,数据才会提交给数据库。 但我们的逻辑还有点问题,需要修改。
如果执行中出错, 我们得进行回滚:

@web.route('/gifts/book/')
@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:
            db.session.rollback()  # 数据库回滚
            raise e
    else:
        flash('这本书已添加至你的赠送清单或已存在于你的心愿清单,请不要重复添加!')
     return redirect(url_for('web.book.detail', isbn=isbn))

如果程序执行到db.session.commit()失败了, 没有进行rollback操作,那么不仅仅这次操作会失败 以后所有的数据库插入操作都会失败。

只要执行db.session.commit() 都进行 rollback。 我们接下来会对这样的代码进行优化。


四. Python @contextmanager

我们以前讲过with上下文机制:

class MyResource:
    def __enter__(self):
        print('connect to resource')
        return self

    def __exit__(self,  exc_type, exc_value, tb):
        print('close resource connection')

    def query(self):
        print('query data')


with MyResource() as resource:
    1/0
    resource.query()

代码的核心其实是 query函数, 我们可以使用 @contextmanager来代替:

class MyResource:
    # def __enter__(self):
    #     print('connect to resource')
    #     return self
    #
    # def __exit__(self,  exc_type, exc_value, tb):
    #     print('close resource connection')

    def query(self):
        print('query data')


# with MyResource() as resource:
#     1/0
#     resource.query()

from contextlib import contextmanager

@contextmanager
def make_myresource():
    print('connect to resource')
    yield MyResource()
    print('close resource connection')


with make_myresource() as r:
    r.query()

五. 灵活使用@contextmanager

print('书名'), 何如给书名加上‘《》’? 我们可以使用@contextmanager:

from contextlib import contextmanager

@contextmanager
def book_mark():
    print('', end='')   # end=''为了去掉换行符
    yield                # 没有用到 with···as 所以不需要返回值
    print('', end='')
    
with book_mark() :
    print('书名', end='')

结果为《书名》


六. 结合继承、yield、contextmanager、rollback来解决问题

接下来解决第三节最后的问题
app/models/base.py:

from flask_sqlalchemy import SQLAlchemy as _SQLAlchemy
from sqlalchemy import Column, Integer, SmallInteger
from contextlib import contextmanager


class SQLAlchemy(_SQLAlchemy):   # SQLAlchemy添加auto_commit方法
    @contextmanager
    def auto_commit(self):
        try:
            yield
            self.session.commit()
        except Exception as e:
            raise e


db = SQLAlchemy()


class Base(db.Model):
    __abstract__ = True
    create_time = Column('create_time', Integer)
    status = Column(SmallInteger)

    def set_attrs(self, attrs_dict):
        for key, value in attrs_dict.items():
            if hasattr(self, key) and key != 'id':
                setattr(self, key, value)

app/web/gift.py的save_to_gifts视图函数:

@web.route('/gifts/book/')
@login_required
def save_to_gifts(isbn):
    if current_user.can_save_to_list(isbn):
        with db.auto_commit():    # 运用新写的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.commit()
    else:
        flash('这本书已添加至你的赠送清单或已存在于你的心愿清单,请不要重复添加!')
     return redirect(url_for('web.book.detail', isbn=isbn))

同样的,只要是db.session.commit的地方都改一下
app/web/auth.py的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)
        # db.session.commit()
        return redirect(url_for('web.login'))  
    return render_template('auth/register.html', form=form)


七.类变量的陷阱

优化app/models/base.py的Base模型, 使得创建实例变量时自动生成创建时间。:

from flask_sqlalchemy import SQLAlchemy as _SQLAlchemy
from sqlalchemy import Column, Integer, SmallInteger
from contextlib import contextmanager
from datetime import datetime


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


db = SQLAlchemy()


class Base(db.Model):
    __abstract__ = True
    create_time = Column('create_time', Integer)
    status = Column(SmallInteger, default=1)

    def __init__(self):  # 增加__init__函数
        self.create_time = int(datetime.now().timestamp())  # 自动生成时间戳

    def set_attrs(self, attrs_dict):
        for key, value in attrs_dict.items():
            if hasattr(self, key) and key != 'id':
                setattr(self, key, value)

问题:如果我们不使用__init__,而是在create_time中增加default参数的方法,能自动生成创建时间吗?
答案: 这样所有的实例化的数据库的数据, 创建时间都一样。在flask程序运行初始化的时候,会db.create_all, 这时候就会自动创建类生成时间,而不是实例的生成时间。


八. 合理利用ajax

我们在save_to_gifts中, 随后会redirect到详情页, 这样的操作其实很奇怪:

1.在详情页点击 赠送此书
2.赠送完成后, 在没有去别的页面的情况下, 又渲染了一遍详情页。

这样的情况,我们就可以使用ajax技术。在点击赠送此书后, 只会发送请求,而不需要重新渲染原来的页面。


九. 书籍交易视图模型

我们会在详情页中显示 赠出书籍的记录: 

9.书籍交易模型(数据库事务、重写flask中的对象)_第2张图片

创建app/view_models/trade.py:

class TradeInfo:
    def __init__(self, goods):  # 传入wishesgifts
        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):
        return dict(
            user_name=single.user.nickname,
            time=single.create_time.strftime('%Y-%m-%d'),  # 存在数据库中的是时间戳,需要转化
            id=single.id
        )


十.处理时间

我们在TradeInfo类中time=single.create_time.strftime('%Y-%m-%d')的create_time得到的是int的时间戳,无法使用strftime方法。 我们修改app/models/base.py的class Base:

class Base(db.Model):
    __abstract__ = True
    create_time = Column('create_time', Integer)
    status = Column(SmallInteger, default=1)

    def __init__(self):
        self.create_time = int(datetime.now().timestamp())  # 自动生成时间戳

    def set_attrs(self, attrs_dict):
        for key, value in attrs_dict.items():
            if hasattr(self, key) and key != 'id':
                setattr(self, key, value)

    @property
    def create_datetime(self):     # int类型转为时间戳
        if self.create_time:
            return datetime.fromtimestamp(self.create_time)
        else:
            return None

修改TradeInfo的__map_to_trade方法:

class TradeInfo:
    def __init__(self, goods):  # 传入wishesgifts
        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
        )


十一. 书籍详情页面

修改app/web/book.py的book_detail视图函数:

@web.route('/book//detail')
def book_detail(isbn):
    has_in_gift = False  # 在礼物清单
    has_in_wish = 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, isbn=isbn, launched=False).first(): # 用户是否是赠送者
            has_in_gift = True
        if Wish.query.filter_by(uid=current_user.id, isbn=isbn, launched=False).first(): # 用户是否是索要者
            has_in_wish = True
    

    trade_gifts = Gift.query.filter_by(isbn=isbn, launched=False).all()
    trade_wishes = Wish.query.filter_by(isbn=isbn, launched=False).all()

    trade_gifts_model = TradeInfo(trade_gifts)
    trade_wishes_model = TradeInfo(trade_wishes)

    return render_template('book_detail.html', book=book, wishes=trade_wishes_model, gifts=trade_gifts_model,
                           has_in_gift=has_in_gift, has_in_wish=has_in_wish)


十五. 重写filter_by

在第二节最后,说filter_by有坑。原因是:

我们项目中删除数据库的数据,其实是通过Base的status置为0的软删除方法
这样一来,如果直接使用filter_by会把删除的数据,一起找到。

当然我可以在filter_by中加入参数status=1,但每次都必须加的话,太不合理了。我们需要改写filter_by

我们使用的filter_by是flask_sqlalchemy的Basequery继承sqlalchemy的orm.query, 在flask_sqlalchemy的初始化的时候,给了我们重写Basequery的机会:
源码SQlAlchemy的__init__:

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

        self.use_native_unicode = use_native_unicode
        self.Query = query_class
        self.session = self.create_scoped_session(session_options)
        self.Model = self.make_declarative_base(model_class, metadata)
        self._engine_lock = Lock()
        self.app = app
        _include_sqlalchemy(self, query_class)

        if app is not None:
            self.init_app(app)

我们自己写一个Query,传给query_class即可
修改app/models/base.py:

from flask_sqlalchemy import SQLAlchemy as _SQLAlchemy, BaseQuery
from sqlalchemy import Column, Integer, SmallInteger
from contextlib import contextmanager
from datetime import datetime


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


class Query(BaseQuery):    # 继承BaseQuery 并重写filter_by
    def filter_by(self, **kwargs):
        if 'status' not in kwargs.keys():
            kwargs['status'] = 1       # **kwargs字典中加入status即可
        return super(Query, self).filter_by(**kwargs)


db = SQLAlchemy(query_class=Query)   # 传入query_class


class Base(db.Model):
    __abstract__ = True
    create_time = Column('create_time', Integer)
    status = Column(SmallInteger, default=1)

    def __init__(self):
        self.create_time = int(datetime.now().timestamp())  

    def set_attrs(self, attrs_dict):
        for key, value in attrs_dict.items():
            if hasattr(self, key) and key != 'id':
                setattr(self, key, value)

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

这样我们使用filter_by的时候就不用传status=1了。

你可能感兴趣的:(flask)