Shawn's Blog

一个伪程序员的伪技术博客

0X00 按惯例得有一个标题

众所周知save是 Django 中最常用的保存数据的方法。但是一般来说大家经常会把“常用“理解成“万能“,然后能用的时候就全用这一种方式。不过编程 中是没有所谓的“一招鲜吃遍天“的,Django 之所以提供了那么多中保存数据的方法也侧面证实了这一点。

首先来看一下我遇到的这个问题:

1
2
3
4
5
6
7
8
from .models import Student
from .utils import calculate_score

queryset = Student.objects.all()
for student in queryset:
student.score = calculate_score(student) # 调用一个工具函数计算该学生的成绩
student.save()

这段代码乍一看没有什么问题,因为计算的值是通过calculate_score这个函数进行的,所以不能使用queryset.update(xxx)的方法。然后咱们看一下 Django 文档是如何描述 queryset 的。

QuerySets are lazy – the act of creating a QuerySet doesn’t involve any database activity. You can stack filters together all day long, and Django won’t actually run the query until the QuerySet is evaluated.

QuerySets are lazy

nternally, a QuerySet can be constructed, filtered, sliced, and generally passed around without actually hitting the database. No database activity actually occurs until you do something to evaluate the queryset.

When QuerySets are evaluated

QuerySets are lazy 内容总结来说就是“Django 中的 QuerySet 只有在用的时候才会真的去数据库里查,而不是生成 QuerySet 的时候“。后面的 When QuerySet are evaluated 则标明了什么叫做“真正在使用“,给出了下面几个条件,当你做这些事情的时候就是“真正在使用“了。这些条件包括:迭代、 切片、列表等(我英文水平小学三年级,解读地不对的地方还希望大家指出)。 所以显然我们对这个 queryset 来了个 for 循环就满足了上述的“迭代“,所以这时候数 Django 就会真正的从数据库中将数据真正的 取出来

现在问题来了。我们思考一个问题,如果我一秒钟能计算10 个学生的成绩,然后整个Student表有 3W 学生,得出“处理所有学生信息需要消耗 50 分钟的时间“这样的结论(每秒 10 条和一共 3W 是乱写的,真实数据通常比这个大得多)。

如果在执行这个循环的时候,某位同学修改了自己的的信息,比如手机号,会发生什么? 有两种可能:第一种可能是这位同学修改自己手机号的时候计算分数的循环已经把他的分数计算完了,那么他的手机号修改也生效了(这种最好)。但是如果他改手机号的时候循环还没到他呢?假设他把手机号从原来的 123 改成了 456,那么他改完手机号的一瞬间数据库里存进去的确实是 456,没有问题。但是 queryset 里是他改手机号以前取出来的 123,这时候循环到他了,计算完之后来了一个student.save(),如此一来他刚刚改好的手机号码就又回到了 123。

所以说这种写法并不会 100% 出现问题。整个循环耗时越久,出现问题的可能性越大;系统中数据变更越频繁,出现问题的可能性越大。当然了,bug 就是 bug,不能因为 bug 没触发就无所畏惧了,还是得解决的。通常来说有两种解决方法,下面是第一种

1
2
3
for student in queryset:
new_score = calculate_score(student)
Student.objects.filter(id=student.id).update(score=new_score)

这种方式仍然比较 young 比较 simple 比较 naive,不过又不是不能用

但是这种用法显然是不好的,而且 update 本来也不是让我们这么用的。所以我们还是得回到save上,Django 其实已经提供了一个参数给 save 了,可以用下面这种方法

1
2
3
for student in queryset:
student.score = calculate_score(student)
student.save(update_fields=['score'])

也就是在 save 的时候带上具体需要更新哪个字段,其他的就不更新了。而且通过传递的参数也可以看出,指定的是多个字段,如果有需要修改多个字段的话,就只修改这一个就好了。

不过其实这里还是有一个潜在问题的,那就是说:恰好我们在更新 score 的时候,其他地方也在更新这个。不过这个更多的时候就是我们程序逻辑的问题了,因为在几乎同一时间对一个字段进行修改,然后修改的双方又互相不知道的话,总是会出问题的。

0X00 Django 中的权限结构、定义

我们知道在创建了一个 Django 项目之后,默认就有两个公开可用的 model:User 和 Group,这两个 model 的一项功能就是用来做权限管理的。系统中会有很多项权限,单个 user 可以配置拥有哪些权限,也可以将权限配置给 group。然后校验单个权限的时候其实就是将 user 本身的权限,和 user 所在的所有组的权限做一个并集,看本次操作的权限是否在这个并集里。在,那就校验通过;不在,那就只有 HTTP 403 了。

HTTP 401 和 HTTP 403 的区别:401 的描述是 Forbidden,而 401 是 Unauthorized,前者是没有权限,而后者干脆没通过认证。举个例子,你想查看公司财务的详细报表,财务经理一看你就是个一线小程序员,就给你一个 401,告诉你这不是你可以看的东西。如果你想看别的公司财务的详细报表,别人公司财务经理一看你根本不是他们公司的人,就直接给你了个 401 了。(俗话说十个比喻九个不准,我这个比喻当然也并不非常准确,不过对于分不清 401 和 403 的同学而言应该也问题不大🤣)

Django 自己对每一个 model 都创建了 create,update,delete 的权限,我们可以直接拿来用,也可以自己添加新权限。Django 自己是针对各个 model 做的权限,所以最简单的权限建立是在 model 层进行的。就比如下面这种,如果我想要为 Student 这个 model 建立相关的权限,就可以通过修改 Meta 类里的 permissions 来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Student(models.Model):
user = models.OneToOneField('django.contrib.auth.models.User')
name = models.CharField() # 纯展示,就不详细定义了
birthday = models.DatetimeField()
phone = models.CharField()

class Meta:
verbose_name = '学生'
verbose_name_plural = verbose_name
permissions = (
# ('权限名', '权限描述'),
('check_classmate_score', '查看同班同学的成绩'),
('send_class_notify', '发送班级通知'),
)

不过这里也看到了,每次对权限进行 CUD 的时候都是在改 model 的,所以每次改动完 model 记得都要进行一次migrate操作才行。不过不用担心性能问题,这个 migrate 只对 Permission 表进行 CUD 操作,而并非改表,所以非常快就搞定了。注意的一点是,不管你把这些 permissions 写在哪个 model 下,最终他们创建好的数据都还是在 Permission 表里,也就是数据库(我这里用的是 MySQL)里的auth_permission表了,这个表结构和数据是下面这样的。

1
2
3
4
5
6
7
8
+-----------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-----------------+--------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | <null> | auto_increment |
| name | varchar(255) | NO | | <null> | |
| content_type_id | int(11) | NO | MUL | <null> | |
| codename | varchar(100) | NO | | <null> | |
+-----------------+--------------+------+-----+---------+----------------+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
+-----+------------------------------------+-----------------+-------------------------------+
| id | name | content_type_id | codename |
+-----+------------------------------------+-----------------+-------------------------------+
| 1 | Can add log entry | 1 | add_logentry |
| 2 | Can change log entry | 1 | change_logentry |
| 3 | Can delete log entry | 1 | delete_logentry |
| 4 | Can add group | 2 | add_group |
| 5 | Can change group | 2 | change_group |
| 6 | Can delete group | 2 | delete_group |
| 7 | Can add permission | 3 | add_permission |
| 8 | Can change permission | 3 | change_permission |
| 9 | Can delete permission | 3 | delete_permission |
| 10 | Can add user | 4 | add_user |
| 11 | Can change user | 4 | change_user |
| 12 | Can delete user | 4 | delete_user |
+-----+------------------------------------+-----------------+-------------------------------+
阅读全文 »

0X00 网站怎么登陆

回忆一下你用过的网站们,一般都是怎么登陆的? “就输入用户名密码登陆呗,要么就扫码登录,要么手机验证码登录,还能有啥”。确实没啥,咱们平时用到的登录方式也就都是这样的,而且这些其实从原理上来说都是 证明你是你 的手段,用户名密码是通过“密码只有你自己知道”为前提的,扫码和验证码是以“手机一定在你自己手上,并且只有你自己能解锁”为前提的。所以其实用户名密码并不是登录的唯一方法,理论上能证明你是你的一切方法都可以用来做授权认证,所以我们可以看到除了密码,出现了扫码、短信邮箱验证码、指纹、人脸、声纹、虹膜巴拉巴拉的。

auth

上面这些都是稳定靠谱的登录方式,我们来想另一个问题,你们有一个网站选用了用户名密码的方式登录,你和用户开开心心登录。后来你上线了另一个网站,你觉得用户该怎么登陆这个新网站呢?“新网站就再注册一次呀!”好的,用户又注册了一次。随着你们公司业务逐渐壮大,上线了 100 个网站,那用户要注册一百次吗?万一自己常用的用户名被占用了呢,岂不是 100 个网站可能会用 100 套不同的用户名密码?那用户可能就要骂娘了呦。

聪明的你可能会想到,既然现在这么流行微服务,我们把登录做一个微服务,所有系统的登陆都用这一套不就好了?

0X01 这就是 SSO

那有一个好消息和一个坏消息,坏消息是,你以为的这个天才想法就是 SSO(Single Sign On)单点登录,早就被人想到而且实现了很多遍了;好消息是,轮子很多,完全不用自己造,挑一个拿来用就好。

单点登录(英语:Single sign-on,缩写为 SSO),又译为单一签入,一种对于许多相互关连,但是又是各自独立的软件系统,提供访问控制的属性。当拥有这项属性时,当用户登录时,就可以获取所有系统的访问权限,不用对每个单一系统都逐一登录。这项功能通常是以轻型目录访问协议(LDAP)来实现,在服务器上会将用户信息存储到LDAP数据库中。相同的,单一退出(single sign-off)就是指,只需要单一的退出动作,就可以结束对于多个系统的访问权限。 –维基百科

阅读全文 »

0X00 运行一个 Django 程序

运行一个 Django 程序可太简单了,从创建项目到运行起来总共也不超过 5 行代码。项目运行起来了就可以打开我们的 vim 或者 IDE 之类的一顿 coding 了。作为最最最开始写 Django 的同学来说到这里也就了解的差不多了,因为大家都是自己写好代码本地测试一下就提 Pul Request 到上游仓库了,然后什么单元测试、数据库迁移、测试环境版本发布甚至可能包含 docker 镜像更新就全都交给 CI 来做了。自己就这么开开心心的写了一段时间的代码,一切都在朝着好的方向发展。突然有一天部门主管或者老大告诉你有一个新项目要你来开个头,先搭好脚手架然后发布上去,后面再来人一起做功能迭代。
运行一个 Django 程序

然后你开开心心地django-admin startproject xxxx、开开心心地django-admin startapp xxxxx,一顿 coding 之后懵逼了,没有部署过测试环境,没有部署过生产环境,只知道 CI 给做了,却完全不知道做了什么。然后你跑去看 CI 脚本,去问其他同事同学,得到了一堆 Nginx 和 uWSGI 之类的答复。你也照着做了,但是完全不知道为什么,因为你觉得python manage.py runserver明明就可以启动项目了,为什么还需要搞什么 Nginx 和 uWSGI 呢?

阅读全文 »

0X00 前言

一见程序员,立刻想到 web 开发,立刻想到后台管理系统,立刻想到数据展示,立刻想到数据筛选筛选,立刻想到数据统计,立刻想到导出 Excel 表格。产品经理的想象惟在这一层能够如此跃进。 –鲁迅:我不是,我没有,别瞎说

我不是,我没有,别瞎说啊

虽然上面这种说法有点夸张了,不过确实很多很多很多人在工作中遇到过不止一次的需要在一个 web 系统里添加一个”数据导出”的功能,而且通常都是导出成 csv 这种文件。自然我也遇到了很多很多很多次,也写过那种最蠢的手拼逗号的 csv 导出,还看过别人效果更好代码量更少的版本。也就在此总结一下具体这个 csv 导出该怎么搞才好。

最蠢的方案可能就是我最早实习的时候写出来的那种手拼逗号的方案了,为了大家刚吃的早午晚饭着想,就不给大家看了,省得吐出来浪费粮食。真正用的比较多的是这么两种:一种是传统的拼接二维数组的方式来模拟表格,然后通过 Python 的 csv 库直接导出;另一种是使用 djcsv 来进行导出。下面来简单看一下嘞。

阅读全文 »
0%