10.1 最近的礼物

我们的首页会显示最近的赠送书籍列表。这个列表有三个限制条件: 1.数量不超过30 2.按照时间倒序排列,最新的排在最前面 3.去重,同一本书籍的礼物不重复出现

1.首先编写复杂SQL对应的ORM代码

由于是最近的礼物,所以应该编写在models/gift.py中

    @classmethod
    def recent(cls):
        # 链式调用 主体是Query ,遇到all(),first()就会终止生成一条sql
        # 建造者模式
        # select distinct * from gift group by isbn order by create_time limit 30
        recent_gifts = Gift.query\
            .filter_by(launched=False)\
            .group_by(Gift.isbn)\
            .order_by(Gift.create_time)\
            .limit(30)\
            .distinct().all()
        return recent_gifts

为什么要定义成类方法呢。

  • 对象代表一个礼物,是具体的
  • 类代表礼物这个事物,他是抽象的,不是具体的一个

2.业务的四种编写方案

1.编写在models的对应的gift.py里。 2.编写在视图函数里。(看你认为当前这段的业务有没有意义) 3.在models里建立新的RecentGift模块。 4.建立service层。(不推荐,Service层全都是静态方法,没有理解面向对象的意义)

3.编写视图函数

我们编写的recent函数获取到的gift列表里的每一个gift,都只有isbn编号。但是我们需要把图书的信息都返回回去。这就需要拿isbn编号去YushBook可去查询出书籍的详情信息然后再使用BookViewModel进行封装。

但是上面这段逻辑,不应该写在视图函数的for循环中,他是Gift的行为,应该封装到Gift的模型中去。

    @property
    def book(self, isbn):
        yushu_book = YuShuBook()
        yushu_book.search_by_isbn()
        return yushu_book.first

不能在Gift模型中直接返回BookViewModel,因为我们的模型只负责处理原始数据,所有的处理ViewModel都应该放到视图函数里面进行。

web/main.py

@web.route('/')
def index():
    recent_gifts = Gift.recent()
    books = [BookViewModel(gift.book) for gift in recent_gifts]
    return render_template('index.html', recent=books)

之所以能够在调用的地方用一个很简单的列表推导式就完成了这么复杂的逻辑,就是因为我们封装的非常良好。这里面涉及到了几个对象的相互调用-bookviewmodel,gift,yushubook;这才是在真正的写面向对象的代码,面向对象就是几个对象在相互调用,各自的逻辑是非常清晰的(单一职责模式)

良好的封装是优秀代码的基础

10.2 我的礼物(赠送清单)

1.业务逻辑分析

赠送清单的业务逻辑如下

image.png

其中复杂在于第二点,实现有以下两种思路

image.png

如果循环次数可以控制,比如10次,100次,那么我们还可以接受,但是这个循环次数是掌握在用户手里的,所以第一种方案是不能够接受的。我们采取第二种方案

2.代码编写

models/gift.py 对原始数据的获取

    @classmethod
    def get_user_gifts(cls, uid):
        gifts = Gift.query \
            .filter_by(uid=uid, launched=False) \
            .order_by(desc(Gift.create_time)) \
            .all()
        return gifts

    @classmethod
    def get_wish_counts(cls, isbn_list):
        # 根据传入的一组isbn编号,到Wish表中计算出某个礼物的Wish心愿数量
        # select count(id),isbn from wish
        # where launched = false and isbn in ('','') and status =1 group by isbn
        count_list = db.session.query(func.count(Wish.id), Wish.isbn).filter(
            Wish.launched == False,
            Wish.isbn.in_(isbn_list),
            Wish.status == 1).group_by(
            Wish.isbn).all()
        # 不要将tuple返回到外部,应该返回有意义的字典或者对象
        count_list = [{'count': w[0], 'isbn':w[1]} for w in count_list]
        return count_list

view_model/gift.py 对原始数的裁剪包装

class Gifts:

    def __init__(self, gifts_of_mine, wish_count_list):
        self.gifts = []
        self.__gifts_of_mine = gifts_of_mine
        self.__wish_count_list = wish_count_list

        self.gifts = self.__parse()

    def __parse(self):
        temp_gifts = []
        for gift in self.__gifts_of_mine:
            my_gift = self.__matching(gift)
            temp_gifts.append(my_gift)
        # 不应该在函数内部对对象的属性进行修改,应该返回给外部,在外部进行复制
        # 这是因为如果函数比较复杂,我们根本不知道对象的属性是在哪个函数中修改的
        return temp_gifts

    def __matching(self, gift):
        count = 0
        for wish_count in self.__wish_count_list:
            if gift.book == wish_count['isbn']:
                count = wish_count.count
        r = {
            'wishes_count': count,
            'book': BookViewModel(gift.book),
            'id': gift.id
        }
        return r

web/gift.py 对视图函数进行组装

@web.route('/my/gifts')
@login_required
def my_gifts():
    uid = current_user.id
    gifts_of_mine = Gift.get_user_gifts(uid)
    isbn_list = [gift.isbn for gift in gifts_of_mine]
    wish_count_list = Gift.get_wish_counts(isbn_list)
    view_model = Gifts(gifts_of_mine, wish_count_list)
    return render_template('my_gifts.html', gifts=view_model.gifts)

上面获取原始数据,是对两张表分别查询,再组装,我们也可以进行连表查询,下面是两种方式

    # 直接进行sql查询
    @classmethod
    def get_user_gifts_by_sql(cls, uid):
        sql = 'select a.id,a.isbn,count(b.id)' \
              'from gift a left join wish b on a.isbn = b.isbn ' \
              'where b.uid = %s and a.launched = 0 and b.launched = 0 ' \
              'and a.status = 1 and b.status = 1 ' \
              'group by a.id,a.isbn order by a.create_time desc'.replace('%s', str(uid))
        gifts = db.session.execute(sql)
        gifts = [{'id': line[0], 'isbn': line[1], 'count':line[2]} for line in gifts]
        return gifts
    
    # 使用SQLAlchemy提供的多表查询的方式
    @classmethod
    def get_user_gifts_by_orm(cls, uid):
        gifts = db.session\
            .query(Gift.id, Gift.isbn, func.count(Wish.id))\
            .outerjoin(Wish, Wish.isbn == Gift.isbn)\
            .filter(
                Gift.launched == False,
                Wish.launched == False,
                Gift.status == 1,
                Wish.status == 1,
                Gift.uid == uid)\
            .group_by(Gift.id, Wish.isbn)\
            .order_by(desc(Gift.create_time))\
            .all()
        gifts = [{'id': line[0], 'isbn': line[1], 'count':line[2]} for line in gifts]
        return gifts

10.3 我的心愿(心愿清单)

我的心愿的代码可以说是我的礼物的镜像代码,只是改下名字。

但是考虑view_models,gift和wish的view_model 可以合并成一个MyTrade。实际上Trade应该是gift和wish的基类,在我们这里他们之间没有行为差异,之间用一个即可,如果他们有了行为差异,就应该分别继承Trade实现自己的业务逻辑

class MyTrade:

    def __init__(self, trades_of_mine, trades_count_list):
        self.trades = []
        self.__trades_of_mine = trades_of_mine
        self.__trades_count_list = trades_count_list

        self.trades = self.__parse()

    def __parse(self):
        temp_trades = []
        for trade in self.__trades_of_mine:
            my_trade = self.__matching(trade)
            temp_trades.append(my_trade)
        return temp_trades

    def __matching(self, trade):
        count = 0
        for trade_count in self.__trades_count_list:
            if trade.isbn == trade_count['isbn']:
                count = trade_count['count']
        r = {
            'wishes_count': count,
            'book': BookViewModel(trade.book),
            'id': trade.id
        }
        return r