第十一章 鱼书业务处理(1/2)



一、编写最近上传页面(复杂SQL的编写方案)

1.1 成品页面的功能

在成品的赠送清单页面:

目前,已完成了搜索结果页面、图书详情页面。接下来,开始编写最近上传这个页面,这也是整个网站的首页。

查询的虽然是礼物,但是显示的却是礼物中的书籍信息。

1.2 编写具体业务逻辑

最近上传的页面应该写在哪里?

在模型层app|models|gift.py中:

...

class Gift(Base):                                         #在gift模型中
    id = Column(Integer, primary_key=True)               # 礼物的唯一标识
    launched = Column(Boolean, default=False)            # 礼物有没有送出去?默认False,礼物没有赠送出去
    user = relationship('User')                          # 引用User这个模型,用relationship表示引用关系
    uid = Column(Integer, ForeignKey('user.id'))         # 具体是引用哪一个用户呢?     # 外键ForeignKey:数据库术语
    isbn = Column(String(15), nullable=False)            # 同一本书可以被赠送多次      # 引用book这个模型

    def recent(self):
        '''最近上传页面的3个具体业务逻辑'''
        recent_gift = Gift.query.filter_by(
            launched=False).group_by(                             # 先根据ISBN进行分组,再去重
            Gift.isbn).order_by(                                # 按照时间倒序排列
            Gift.create_time).limit(                            # 只显示一定数量(30)
            current_app.config['RECENT_BOOK_COUNT']).distinct().all()
        return recent_gift

先分组group_by(),再去重distinct()

先将整体按照时间倒序排列order_by(),再筛选其中的前30条limit()

1.3 链式调用

上面是一种优秀的代码写法,叫链式调用。

1.其特点是:

(1)存在一个主体。

例如,Query对象

(2)要有一些子函数。

例如,filter_by()order_by()

子函数是任意多个。所有的子函数都会返回上面的主体Query对象

(3)要有触发语句。

例如,all()first()

它不属于子函数。一旦链式调用的主体遇到了all(),就会生成一条SQL语句,去数据库中执行具体的查询。

2.优势:

提供了极大的灵活性

1.4 比较四种编写方案

分析把recent()函数放到模型Gift下的合理性:

recent()是一个实例方法,表示取最近的30条礼物记录。一个被实例化的Gift对象代表的是一个礼物。

一个礼物里面有取多个礼物的方法是不合适的。因此,应该将recent()实例方法变为类方法。

上述写法1改为:

在模型层app|models|gift.py中:

...

class Gift(Base):
    id = Column(Integer, primary_key=True)               
    launched = Column(Boolean, default=False)            
    user = relationship('User')                        
    uid = Column(Integer, ForeignKey('user.id'))         
    isbn = Column(String(15), nullable=False)         

    @classmethod                                        # 变为类方法
    def recent(cls):
        '''最近上传页面的3个具体业务逻辑'''
        recent_gift = Gift.query.filter_by(
            launched=False).group_by(
            Gift.isbn).order_by(
            desc(Gift.create_time)).limit(
            current_app.config['RECENT_BOOK_COUNT']).distinct().all()
        return recent_gift                                # 形式是:[<Gift 3>, <Gift 4>, <Gift 5>]

写法2:

上面的本质,只是做了一次sql的查询,没啥具体的业务意义,可以将其写在视图函数中。

能提取出具体的业务意义:写在模型层

不能提取出具体的业务意义:写在控制层的视图函数中

写法3:

跟写法1一样,还是写在模型层。不过单独成模块

写法4:

写到服务层service,但是这些类只有行为没有特征,还是没能理解OOP的编程思维。

综上:

推荐:写法1

可以:写法2、写法3

不可以:写法4

1.5 在视图函数中调用模型层的recent()函数

模型层中的recent()函数已经写完,需要在视图函数中调用它

在模型层app|web|main.py中:

...

@web.route('/')
def index():
    recent_gifts = Gift.recent()                                    # 调用模型层中的recent()函数
    books = [BookViewModel(gift.book) for gift in recent_gifts]
    return render_template('index.html', recent=books)

在浏览器中成功访问到最近上传页面:

为什么我可以在调用的地方,用很简单的列表推导式就能够完成这么复杂的转换?

因为封装良好,这才是真正的在写面向对象的代码。其实就是很多个对象相互调用,逻辑清晰。

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

二、编写赠送清单页面

上节完成了最近上传页面,接下来继续编写赠送清单和心愿清单两个页面。

2.1 成品页面的功能

逻辑图示:

本项目采用第二种思路,难点在于需要合并两个列表。

在成品的赠送清单页面:

点击某本书后,应该跳转到该书的详情页面。

2.2 拿到赠送的礼物清单

业务逻辑:根据用户的id号,查询出该用户所有的礼物

在模型层app|models|gift.py中:

...

class Gift(Base):
    id = Column(Integer, primary_key=True)            
    launched = Column(Boolean, default=False)          
    user = relationship('User')                          
    uid = Column(Integer, ForeignKey('user.id'))     
    isbn = Column(String(15), nullable=False)         
    
    @classmethod
    def get_user_gifts(cls, uid):                            # 编写新方法
        '''根据用户的id号,查询出该用户所有的礼物'''
        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):                    
        pass

在视图函数app|web|gift.py中:

...

@web.route('/my/gifts')
@login_required                                  # 用户必须登录才能访问     
def my_gifts():
    uid = current_user.id
    gifts_of_mine = Gift.get_user_gifts(uid)    # 调用上面刚写好的方法

上述已拿到礼物的清单,但是,想要该礼物的数量还没有。

2.3 用另一种查询方式,获得想要该礼物的人的数量

以前的查询,是查询模型;现在的查询,是查询模型的数量。

1.两种查询方式filter_byfilter()有什么不同?

filter_byfilter()
使用方式Gift.query.filter_by()db.session.query().filter()
传入参数接受的是关键字参数接受的是条件表达式
使用场景单纯的查询模型,很快速当查询比较复杂、尤其是跨模型跨表查询
举例gifts = Gift.query.filter_by(uid=uid, launched=False).order_by( desc(Gift.create_time)).all()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()

db.session:不止可以用来做保存操作,还可以删除、查询。

2.使用db.session.query().filter()进行查询

app|models|gift.py中:

...

class Gift(Base):
    id = Column(Integer, primary_key=True)            
    launched = Column(Boolean, default=False)          
    user = relationship('User')                          
    uid = Column(Integer, ForeignKey('user.id'))     
    isbn = Column(String(15), nullable=False)         
    
    @classmethod
    def get_user_gifts(cls, uid):
        '''根据用户的id号,查询出该用户所有的礼物'''
        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):             # 拿到想要该礼物的人的数量
        #用db.session.query().filter()查询
        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(                   # group_by与func.count()连用,叫做分组统计
            Wish.isbn).all()
        return count_list                             # 返回的是列表

上面已获得想要该礼物的人的数量。但是应该返回什么类型的数值呢?

2.4 三种返回:列表、对象、字典?

1.列表或元组

count_list的结构其实是这样的:[(3,isbn1), (2,isbn2), (5,isbn3)...]

不建议在函数中返回列表或元组的数据结构,因为别人使用只能这样:count_list[0][1],别人看不懂、可读性差。

2.对象

最容易想到的是返回一个对象,对象可以定义属性,每个属性都是有具体意义的

class A():
    count 
    isbn

python中用有一个快速定义对象的方式:

使用namedtuple

from collections import namedtuple

EachGiftWishCount = namedtuple('EachGiftWishCount', ['count', 'isbn'])      # 定义对象的名字    # 定义对象下的相关属性

@classmethod
def get_wish_counts(cls, isbn_list):
    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()
    count_list = [EachGiftWishCount(w[0], w[1]) for w in count_list]
    return count_list

感觉还是不如字典好用。

3.字典

最经典就是用字典来解析。

...

class Gift(Base):
    id = Column(Integer, primary_key=True)            
    launched = Column(Boolean, default=False)          
    user = relationship('User')                          
    uid = Column(Integer, ForeignKey('user.id'))     
    isbn = Column(String(15), nullable=False)         
    
    @classmethod
    def get_user_gifts(cls, uid):
        '''根据用户的id号,查询出该用户所有的礼物'''
        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):                                          # 拿到想要该礼物的人的数量
        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()
        count_list = [{'count': w[0], 'isbn': w[1]} for w in count_list]            # 返回的是字典
        return count_list                             

上面拿到的仍是原始数据,但是还要转换为view_model,以方便在模板中进行渲染。

新建目录:


app|view_models|gift.py中:

from collections import namedtuple
from app.models import gift
from app.view_models.book import BookViewModel

MyGift = namedtuple('MyGift', ['id', 'book', 'wishes_count'])         # 用namedtuple函数,快速完成类:单个

class MyGifts:                                       # 表集合的概念
    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引用即可,否则你还得一个个传进下边的函数中

        self.__parse()                               # 调用下下面的方法


    def __parse(self):                              # 定义一个函数,用于解析上述的列表
        for gift in self.__gifts_of_mine:
            my_gift = self.__matching(gift)
            self.gifts.append(my_gift)                # 注意此时的append的用法!


    def __matching(self, gift):
        count = 0                                   # 要找的数量的初始值
        for wish_count in self.__wish_count_list:
            if gift.isbn == wish_count['isbn']:
                count = wish_count['count']
        my_gift = MyGift(gift.id, BookViewModel(gift.book), count)
        return my_gift

上述,不建议在函数内部直接修改实例的属性,而是通过函数的调用把结果返回回来,然后在函数外边,对实例的属性进行赋值。

即,读取可以,但修改不行。

改为:

app|view_models|gift.py中:

from collections import namedtuple
from app.models import gift
from app.view_models.book import BookViewModel

MyGift = namedtuple('MyGift', ['id', 'book', 'wishes_count'])         

class MyGifts:                                       
    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)                    # 注意append的用法!a.append(b),没有等号!!
        return temp_gifts                                # 返回这个暂时的变量,以临时装载


    def __matching(self, gift):
        count = 0             
        for wish_count in self.__wish_count_list:
            if gift.isbn == wish_count['isbn']:
                count = wish_count['count']
        my_gift = MyGift(gift.id, BookViewModel(gift.book), count)
        return my_gift            # 返回的是列表,其实不太方便以后有可能的序列化

为方便以后有可能的序列化,建议此处返回一个字典:

app|view_models|gift.py中:

from collections import namedtuple
from app.models import gift
from app.view_models.book import BookViewModel

MyGift = namedtuple('MyGift', ['id', 'book', 'wishes_count'])         

class MyGifts:                                       
    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.isbn == wish_count['isbn']:
                count = wish_count['count']
        r = {                                           # 表示单个          # 以字典的形式返回
            'wishes_count': count,
            'book': BookViewModel(gift.book),
            'id': gift.id
        }                               
        return r

在浏览器中成功访问到赠送清单页面:

注意:要想有上面的效果,必须有两个以上用户方可。

bug记录:

问题报错:

❌error: object of type 'NoneType' has no len()

错误分析:

调试发现:view_model.gifts根本就是没有值,是None!None是没有长度的。背后的根源在于属于视图模型MyGifts里面的append使用有误!

解决方案:正确的用法

教训:

七月老师擅长代码重构教学。凡重构的地方,一定有改动,稍一疏忽哪个地方没改,极易后面报错。

不只是本项目,在其他项目中也经常会遇到这两种思路的选择,建议使用第二种思路。

因为第一种不可控,非常消耗数据库的性能。

网站的页面功能看起来简单,实际编写并不简单。

声明:Jerry's Blog|版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA协议进行授权

转载:转载请注明原文链接 - 第十一章 鱼书业务处理(1/2)


Follow excellence, and success will chase you.