介绍
当我们查看Web应用程序时,我们经常将它们视为组织良好的系统,这些系统是由许多合作和谐合作的组件制成的。这些组件相互交互的方式有很多。例如,某些组件交互可能是由客户端操作触发的,而某些组件是通过包含具有嵌套函数的方法在内部触发的。 Django本身已经嵌入了背景过程,这些过程不断与其框架的多个部分通信,以使所有内容保持同步和工作顺序。在本文中,我们旨在利用Django的内置和自定义信号来控制应用程序中的事件。
Django信号概述
在Django框架中,我们已经具有由框架本身预定的内部组件逻辑序列。例如,让我们考虑从'django.db.models'中的“ save()”方法。我们知道,一旦我们在模型上调用create()方法(如果我们将create()方法解释为save()方法的包装器),将自动触发save()方法。一种方法间接联系了另一种方法。借助Django信号,可以类似地重新创建和自定义该行为,以促进Django组件之间的更多互连性。以Django信号为我们的通知器,我们可以通过回调函数触发某些事件。
我们甚至可以将Django信号与JavaScript听众进行比较。这个概念相似,我们在JavaScript中有一个听众等待触发事件。同时,Django信号由接收器和发件人组成。我们注册我们的回调,这些回调已准备好接收发件人的输入。
项目设置
为了示例我们将要编写和完成,我们将使用以前的文章“ Improving Database Accessibility”中使用项目设置“ Speedster”。可以找到我们的项目启动和运行的说明。对于信号代码,官方Django文档说以下内容:
严格来说,信号处理和注册代码可以在您喜欢的任何地方生活,尽管建议避免应用程序的根模块及其模型模块以最大程度地减少导入代码的副作用。”
为了使事情保持良好和结构化,让我们在我们的django应用程序“快速”中创建一个python软件包,只是为了我们的信号外壳。我们将命名文件夹“信号”,文件夹本身将包含' init .py'。现在,如果我们愿意,可以将这种隔离扩展到其他部分,例如将“型号”转换为单个模型捆绑包(他们自己的“ .py”文件)并将它们存储在python软件包中:
speedy/
migrations/
signals/
__init__.py
employees_signals.py
models/
employees.py
...
__init__.py
...
__init__.py
这一切都取决于我们的项目结构偏好。我们必须忘记在我们新创建的' init .py'中导入python软件包中的文件:
Python软件包:信号/员工
from .employee_assignment import *
最后一件事(如果您决定上述建议的结构),请注意循环导入。我们在“ init .py”中设置的进口顺序。
Django内置
根据我的研究,最常见的信号用法是用于模型中的事件控制(或操纵模型实例)。我们区分了三组自我解释的信号:
-
pre_save/post_save
-
pre_delete/post_delete
-
pre_init/post_init
上述所有集合源自'django.db.models.signals'。不太熟悉和使用(但仍然有用)集M2M_CHANG和CLASS_PERAKER。有关它们的更多信息,可以在以下参考文献[1]中找到更多信息。在上面提到的集合中,我们可以在相关模型之间创建事件序列,并使其反应性。让我们看看这些内置的行动。
示例:员工分配自动化和维护
在此示例中,我们将为新创建的员工自动创建轮班(如果尚未存在)或与新员工更新现有的班次。一班最多可以有三名员工。如果您从上面的链接遵循项目设置,那么您会注意到对模型的略有更改以适应此示例。
员工py:
class Employees(models.Model):
first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100)
address = models.CharField(max_length=200)
age = models.PositiveSmallIntegerField()
date_of_birth = models.DateField(auto_now_add=True)
class Meta:
indexes = [
models.Index(fields=['id'])
]
shifts.py:
class Shifts(models.Model):
employees_shift = models.ManyToManyField(Employees, through='ShiftsEmployees', related_name='shifts')
wage_bonus = models.DecimalField(max_digits=4, decimal_places=2, default=50.00)
total_hours = models.IntegerField(default=8)
shift_date = models.DateField(default=now)
class ShiftsEmployees(models.Model):
employee = models.ForeignKey(Employees, on_delete=models.CASCADE, null=True)
shift = models.ForeignKey(Shifts, on_delete=models.CASCADE)
class Meta:
unique_together = ('employee', 'shift')
我还没有找到一种直接调用我们模型类元中Manytomanyfield的“ unique_together”属性的方法。因此,我进行了轻微的解决方法,在其中为M2M字段定义了“通过”表。并且有了直接访问外键,两个密钥都添加了unique_together。我们可以注意到该模型与默认值完全相同 - 它只是具有unique_todecter属性。
可选(和主题):当我们声明“通过”模型时,通常会错过一些轻微优化的机会。当我们为外国钥匙添加unique_together时,我们还可以在这些密钥中添加索引。请注意,考虑到我们可能不会使用查找中的ID字段,分配“通过”表的ID可能是不必要的。如果我们使用这些FKS进行查找,那可能会派上用场。
# Optional
indexes = [
models.Index(fields=['employee', 'shift'])
]
考虑到提到的Django内置与模型事件有关,我们已经完成了一半的任务。剩下的就是写信号逻辑。
django的信号检测模型实例上的事件,因此pre_save事件将发生在post_save之前。与发送者“员工”有关的所有信号将写在雇员_assignment.py。
中。lightee_assignment.py:
- lightee_created():
在雇员_created()信号中,执行了多个查询,乍一看似乎并没有什么大不了的。但是,我们应该牢记大局,而我的意思是,每当我们在模型实例上称为save()方法时,我们都会称这些信号。对我们可以用信号塞满了多少逻辑(以及扩展查询)没有限制 - 但我相信我们不应该被带走。例如,客户端单击创建简单的X对象,而“创建”徽标正在旋转10 15秒。这可能是具有多个“填充”信号的潜在结果。如果我们密切关注UX评级,请记住一些事情。
@receiver(post_save, sender=Employees)
def employee_created(sender, instance, created, **kwargs):
# Check if employee was created
if created:
# Find first free shift for newly created employee
try:
# Check if shift is under three employees
# Order by 'shift_date' and get first object
free_shift = Shifts.objects.annotate(employee_count=Count('employees_shift')) \
.filter(employee_count__lt=3).order_by('shift_date').first()
except (Shifts.DoesNotExist, IndexError, Exception,) as e:
print('free_shift Error: ', e)
free_shift = None
# Check if empty shift exists
if free_shift:
# Assign employee to empty shift
free_shift.employees_shift.add(instance.id)
else:
# No free shift found.
# Find the shift with the latest date, and add shift object with: 'latest date' + 1 day
try:
try:
latest_date = Shifts.objects.order_by('-shift_date').values('shift_date').first()
latest_date = latest_date['shift_date'] + datetime.timedelta(days=1)
except (Shifts.DoesNotExist, IndexError, Exception) as e:
print('latest_date Error: ', e)
latest_date = None
# Check if there is at least one object in DB (Fresh DBs)
if latest_date:
new_shift = Shifts.objects.create(shift_date=latest_date)
# Save object
new_shift.save()
# Add M2M relation
new_shift.employees_shift.add(instance.id)
else:
# There aren't any Shifts objects in DB, default will input current date
new_shift = Shifts.objects.create()
new_shift.save()
new_shift.employees_shift.add(instance.id)
except (Shifts.DoesNotExist, Exception) as e:
print(e)
- pie_expired_shifts():
在我们数据库中当前日期之前的所有变化都被认为是冗余的。除非我们有一个自动序列来更新过期对象的数据,否则这些对象是不可重复使用的。现在,我们可以辩论哪个更快的速度,创建对象或对象进行更新 - 但是我离题了。为了保持简单,让我们选择删除对象并重新创建它们的选项。通过擦拭数据进行维护带来许多好处。我们不仅可以在DB中释放存储空间,而且还通过与DB中的数据进行更少的比较来加速查询。
@receiver(pre_save, sender=Employees)
def wipe_expired_shifts(sender, **kwargs):
current_date = datetime.date.today()
try:
Shifts.objects.filter(shift_date__lt=current_date).delete()
except(Shifts.DoesNotExist, Exception) as e:
print('Wipe redundant shifts Error: ', e)
我们几乎完成了:)。我们需要做的最后一件事是将信号导入相关应用程序的Ready()函数中。请注意,我们正在导入一个python软件包,而不是单个文件,如果我们记得, init .py .py。
class SpeedyConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'speedy'
def ready(self):
import speedy.signals
可以通过在员工模型上创建一个简单的基于函数的视图并调用.create()方法来测试代码。这样:
def test_signals(request):
Employee.objects.create(<<insert_test_data>>)
return HttpResponse()
这是直接调用信号的替代方法。例如,我们可以使用内置事件post_save并使用模型实例附加自定义功能:
post_save.connect(my_custom_function, sender=Employees)
模型方法与内置信号
如果考虑到它,则可以轻松地将信号回调函数放入的所有逻辑中,都可以轻松地放入Save()方法的自定义模型方法或覆盖。两者都可以通过模型实例执行一个操作,该模型实例完全相同,并且在调用时都会触发。那么,当我们可以将所有逻辑放入模型的简单方法时,我们为什么要打扰信号配置?
这是我们从移动模型方法逻辑到信号的回调功能的一些很酷的好处:
-
允许在不同模型之间进行毫无疑问的合作 - 信号更容易连接到模型,而无需编辑模型
-
可重复使用的应用程序更喜欢信号,因为更容易适应新环境,除了所有模型都可以使用的信号与嵌入式模型方法不同,
-
官方django文档指出,删除批量操作不使用覆盖模型方法(有关更多信息,请检查参考[2])
-
质量slug&已经生成的数据处理
-
循环导入错误的绝佳解决方案
也应该提到,如果我们不打算连接多个模型或其他组件,我们将不会从信号中受益。因此,如果逻辑包含在单个模型中 - 那么我们只创建模型方法或覆盖现有的方法。尽管如此,如果我们决定深入信号并将所有东西移到那里,就不会造成任何伤害。结果将相同。
经验法则:
-
如果任务与手头模型完全相关,请将逻辑保留在模型本身内。无需暗示信号。我们覆盖我们的save()方法,做我们的事情,并在末尾致电super(pre_save/post_save情况)
-
如果任务涉及其他模型(换句话说,任务与多个模型有关),则信号将非常适合任务
自定义信号
即使我们可以在回调功能中执行各种“魔术”,但信号的主要意图是充当通知器。信号应解释为已有逻辑的附加组件。自定义信号中的事件的工作方式与内置相同。我们有pre_event_x和post_event_x情况。让我们为下一个示例做准备。
我们首先在我们的快速/信号python软件包中创建一个custom_signals.py文件。接下来,我们打开 init .py,并添加一行以从custom_signals.py:
导入全部
from .custom_signals import *
我们都设置了。让我们从一个简单的记录事件示例开始。
示例:应用程序记录
我们将首先声明一个简单的自定义记录功能。在custom_signals.py中,我们声明log_event()函数:
def log_event(message):
# Creates file 'log_event.log' in project root with append mode 'a'
f = open('log_event.log', 'a')
# Write current timestamp with user submitted message
f.write(datetime.now().strftime("%d_%m_%Y_%H_%M_%S") + ' -- ' + message + '\n')
f.close()
该函数创建文件或附加到现有文件,该文件是用户发送的自定义消息。创建的文件位于项目根中。接下来是定制信号设置,该设置有三个步骤:信号声明,接收器设置和连接。声明和接收器我们将位于log_event()函数下方,如这样:
def log_event(message):
# Creates file 'log_event.log' in project root with append mode 'a'
f = open('log_event.log', 'a')
# Write current timestamp with user submitted message
f.write(datetime.now().strftime("%d_%m_%Y_%H_%M_%S") + ' -- ' + message + '\n')
f.close()
# Declare signals
event_signal = django.dispatch.Signal()
@receiver(event_signal)
def view_event_logging(sender, **kwargs):
print(sender, kwargs['message'], kwargs['request'])
message = '{} -- {}'.format(sender, kwargs['message'])
log_event(message)
正如我们所看到的,声明非常简单,但是在接收器上,我们应该指出一些事情。与内置的内置不同,我们不附加发件人参数,只有我们的上述信号。在自定义信号中,发件人可以是任何东西 - 这意味着我们可以(至少在最小值)中传递一个简单的字符串,该字符串指示该信号来自何处。现在,对于回调函数本身,我们可以根据需要发送尽可能多的参数。回调函数打印出已发送的参数,从它们中形成消息并调用log_event()函数。这应该足以开始。
实际上,可以从任何地方发送信号。让我们通过创建一个简单的视图来证明这一点:
def test_logging(request):
# Empty string
message = ''
try:
# Create an object
emp_inst = Employees.objects.create(first_name='user1',
last_name='last_user',
address='address',
age=93)
message += 'Employees object created: {} {}'.format(emp_inst.first_name, emp_inst.last_name)
except (Employees.DoesNotExist, ValueError) as e:
print('Error during employees creating occured: ', e)
# Convert ValueError error to string for successful concatenation
message += str(e)
emp_inst = None
pass
event_signal.send(sender='View: test_logging', message=message, instance=emp_inst)
return HttpResponse()
我们的视图返回一个空的httpresponse(),该httpresponse()曾经添加到urls.py并访问时 - 创建一个员工对象,并以成功/错误消息和新创建的实例发送我们的信号。
一些正在写入log_event.log的示例数据,错误和成功:
01_11_2022_01_15_45 -- View: test_logging -- Field 'age' expected a number but got 'age'. -- None
14_11_2022_14_31_12 -- View: test_logging -- Employees object created: user1 last_user -- Employees object (72)
如果我们仔细查看代码 - 您可能会想知道并说:“为什么不将log_event()直接调用并删除冗余信号中间人?”这是一个好思考。对于某些情况,信号似乎是完美的,而对于其他情况来说是多余的。这本质上是我们要做的事情的本质。在那些怀疑的时刻,投射的力量极为重要。如果我们认为上面的书面信号已经完成并且不会获得进一步的功能升级 - 那么我们可以肯定地结论,不需要。但是,如果我们决定引入其他模型和其他Django组件的逻辑,这些模型可能会与其他代码的其他导入,元素和段相冲突,则在这种情况下,从Django信号中获得额外的可访问性将派上用场。将其视为以前做过相同逻辑的替代路线。
结论
django信号在每种情况下都不应使用。如果代码变得太大且难以理解,调试和测试可能会变得非常复杂甚至混乱。这直接来自官方的Django文档(参考[0]),他们声明我们应该选择直接代码调用,而不是将信号作为组件之间的中介。如果需要,信号是一个很好的选择。我们可以快速设置它,并像以前一样将其作为其他方法写入。它的用法是情境。编程世界正在不断发展,有新的客户需求以及面对的新挑战 - 拥有另一种工具和额外的知识,应该使我们能够以优雅的方式覆盖更多的情况。
参考
[0]“ docs.djangoproject.com”,“信号”,https://docs.djangoproject.com/en/4.1/topics/signals/
[1]“ docs.djangoproject.com”,“信号(库详细信息)”,https://docs.djangoproject.com/en/4.1/ref/signals/
[2]“ docs.djangoproject.com”,“覆盖预定义的模型方法”,https://docs.djangoproject.com/en/dev/topics/db/models/#overriding-predefined-model-methods
[3]“ lexev.org”,“ django:信号或模型方法?”,http://www.lexev.org/en/2016/django-signal-or-model-method/
[4]“ django-advanced-training.readthedocs.io”,“创建和触发自定义信号”,https://django-advanced-training.readthedocs.io/en/latest/features/signals/
[5]“ codeunderscored.com”,“ django中的自定义信号”,https://www.codeunderscored.com/custom-signals-in-django/,2022年3月17日,汉弗莱