一、编写最近上传页面(复杂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_by
与filter()
有什么不同?
filter_by | filter() | |
---|---|---|
使用方式 | 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
,以方便在模板中进行渲染。
新建目录:
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
使用有误!
解决方案:正确的用法
教训:
七月老师擅长代码重构教学。凡重构的地方,一定有改动,稍一疏忽哪个地方没改,极易后面报错。
不只是本项目,在其他项目中也经常会遇到这两种思路的选择,建议使用第二种思路。
因为第一种不可控,非常消耗数据库的性能。
网站的页面功能看起来简单,实际编写并不简单。
Comments | NOTHING