# django特定model保存记录时触发动作

当REST接口提交了model记录之后，希望能够触发一个特定的操作。

Django支持定制`save()`相关的操作哦，例如使用`post_save()`信号或者 `pre_save()`。

在`models.py`中添加

```python
@receiver(pre_save, sender=MainModel)
def save_a_historicmodel(sender, **kwargs):
    #do your save historicmodel logic here
```

或者

```python
def save_a_historicmodel(sender, instance, created, **kwargs):
    print "Post save was triggered! Instance:", instance

signals.post_save.connect(save_a_historicmodel, sender=MainModel)
```

这样，每次`MainModel`保存时候信号都会触发。

> 详细文档见 [post\_save](https://docs.djangoproject.com/en/1.11/ref/signals/#django.db.models.signals.post_save)

## Django Signals

Django Signals是一个当某些事件发生时，允许解耦的应用程序（`decoupled applications`）获得通知的策略。

> `decoupled applications` - 解耦应用程序
>
> 低耦合，高内聚---模块之间低耦合，模块内部高内聚。一个系统有多个模块组成，在划分模块时，要把功能关系紧密的放到一个模块中(高内聚)，功能关系远的放到其它模块中。模块之间的联系越少越好，接口越简单越好(低耦合，细线通信)。如果划分的模块之间接口很复杂，说明功能划分得不太合理，模块之间的耦合太高了，同时也说明某模块的内聚也不高。 - [程序设计经常提到的解耦，到底是要解除什么之间的耦合？](https://www.zhihu.com/question/20278169)

例如，每次一个给定的模型（Model）实例被更新需要废弃掉一个缓存页面，但是，基于这个模型的一系列位置需要更新。此时可以使用`singals`，hook一些代码片段在这个指定model保存动作被触发时执行。

另一种常用的案例是使用Profile策略通过一对一（one-to-one）关系来扩展定制Django User。通常会使用一个信号调度器（`signal dispatcher`）来监听用户的`post_save`事件以便能够更新Profile实例。 -- 可以参考[How to Extend Django User Model](https://simpleisbetterthancomplex.com/tutorial/2016/07/22/how-to-extend-django-user-model.html#onetoone)

### Django Signals工作原理

`Observer Design Pattern`（观察者设计模式）

在信号机制中有两个关键因素：发送者和接收者。正如名字所示，发送者是负责调度一个信号，接收者则是负责接受信号并作一些事情。

`receiver`接收者必须是一个功能模块（`function`）或者是一个实例方法(`instance method`)才能接收信号。

`sender`发送者必须是一个Python对象，或者是None才能从任何发送者这里接收事件。

连接发送者和接收者之间的是"信号调度器"（`signal dispatchers`），是通过`connect`方法的`Signal`实例。

Django核心已经定义了`ModelSignal`，也就是一个`Signal`子类，允许发送者以`app_label.ModelName`形式快速指定为一个字符串。但是，你需要使用`Signal`类来创建定制的信号。

要接收一个信号，需要注册一个`receiver`功能，这样当信号被通过`Signal.connect()`方法发送的时候来获取调用，

### Django Signal使用

这里举例内建信号`post_save`。这段代码位于`django.db.models.signals`模块。这个信号在模型完成`save`方法后触发：

```python
from django.contrib.auth.models import User
from django.db.models.signals import post_save

def save_profile(sender, instance, **kwargs):
    instance.profile.save()

post_save.connect(save_profile, sender=User)
```

> `contrib`意思是**捐献**，通常捐献的开源代码位于这个目录下

以上案例中，`save_profile`就是接收者功能模块，`User`是发送者而`post_save`是信号。可以解读为：每次当一个`User`实例完成执行它的`save`方法后，则`save_profile`功能就被执行。

如果抑制`sender`参数，类似：`post_save.connect(save_profile)`，则`save_profile`功能就会在**任何**Django模型执行`save`方法的时候执行。

另一种注册信号的方法是使用`@receiver`装饰器（decorator）

```python
def receiver(signal, **kwargs)
```

这里`signal`参数可以是一个信号实例（`Signal` instance）或者是一个信号实例的列表/元组（a list/tuple of `Signal` instances）。

> `装饰器`（decorator）是指代码运行期间动态添加功能的方式（不修改函数的定义） - [廖雪峰的Python 2.7教程：装饰器](https://www.liaoxuefeng.com/wiki/001374738125095c955c1e6d8bb493182103fac9270762a000/001386819879946007bbf6ad052463ab18034f0254bf355000)

```python
from django.contrib.auth.models import User
from django.db.models.signals import post_save
from django.dispatch import receiver

@receiver(post_save, sender=User)
def save_profile(sender, instance, **kwargs):
    instance.profile.save()
```

如果希望注册 receiver function到不同的信号，可以如下：

```python
@receiver([post_save, post_delete], sender=User)
```

### Django Signal代码的位置

根据你注册应用程序信号的不同位置，可能由于导入代码产生不同的边缘影响。所以，建议避免将`Signal`代码放在`models`方法或者应用程序的根方法中。

#### 使用`@receiver()`装饰器

Django文档建议将信号存放在app的配置文件。以下是一个名为`profiles`的Django app：

* `profiles/signals.py`:

```python
from django.contrib.auth.models import User
from django.db.models.signals import post_save
from django.dispatch import receiver

from cmdbox.profiles.models import Profile

@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
    if created:
        Profile.objects.create(user=instance)

@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
    instance.profile.save()
```

* `profiles/app.py`:

```python
from django.apps import AppConfig
from django.utils.translation import ugettext_lazy as _

class ProfilesConfig(AppConfig):
    name = 'cmdbox.profiles'
    verbose_name = _('profiles')

    def ready(self):
        import cmdbox.profiles.signals  # noqa
```

* `profiles/__init__.py`:

```python
default_app_config = 'cmdbox.profiles.apps.ProfilesConfig'
```

在以上案例中，只时在`ready()`方法中导入的`signals`模块会工作，因为这里使用了`@receiver()`装饰器。如果使用`connect()`方式来连接`receiver function`，则参考以下案例。

#### 使用`connect()`方法

* `profiles/signals.py`:

```python
from cmdbox.profiles.models import Profile

def create_user_profile(sender, instance, created, **kwargs):
    if created:
        Profile.objects.create(user=instance)

def save_user_profile(sender, instance, **kwargs):
    instance.profile.save()
```

* `profiles/app.py`:

```python
from django.apps import AppConfig
from django.contrib.auth.models import User
from django.db.models.signals import post_save
from django.utils.translation import ugettext_lazy as _

from cmdbox.profiles.signals import create_user_profile, save_user_profile

class ProfilesConfig(AppConfig):
    name = 'cmdbox.profiles'
    verbose_name = _('profiles')

    def ready(self):
        post_save.connect(create_user_profile, sender=User)
        post_save.connect(save_user_profile, sender=User)
```

* `profiles/__init__.py`:

```python
default_app_config = 'cmdbox.profiles.apps.ProfilesConfig'
```

### `instance`使用（补充）

现在有一个问题，就是如果通过`Signals`触发了某个动作，而这个动作恰好就是要处理刚才`Model`中`post_save`保存的那行记录，该如何处理呢？

这里甬道的就是`instance`，这个`instance`就是刚才保存的那行数据库记录。举例：

* `signals.py`

```python
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db.models.signals import post_save
from django.dispatch import receiver

from notify.views import send_msg
from .models import PizzaCooked

@receiver(post_save, sender=PizzaCooked)
def create_pizzacooked_notify(sender, instance, created, **kwargs):
    if created:
        customer = instance.customer
        pizza_name = instance.pizza_name
        pizza_size = instance.pizza_size
        cook_time = instance.cook_time
        msg = "通知：\r亲爱的%s，您订购的 %s %s 披萨已经于 %s 制作完成，请取用" % (customer, pizza_name, pizza_size, cook_time)
        send_msg(msg)
```

以上案例，即通过Signal的`post_save`触发消息发送，其中消息发送内容就是从`instance`也就是数据库插入数据字段组合得到。

## Django内建信号

以下是一些有用的Django信号（常用）

### Model Signals

* django.db.models.signals.`pre_init`:

```python
receiver_function(sender, *args, **kwargs)
```

* django.db.models.signals.`post_init`:

```python
receiver_function(sender, instance)
```

* django.db.models.signals.`pre_save`:

```python
receiver_function(sender, instance, raw, using, update_fields)
```

* django.db.models.signals.`post_save`:

```python
receiver_function(sender, instance, created, raw, using, update_fields)
```

* django.db.models.signals.`pre_delete`:

```python
receiver_function(sender, instance, using)
```

* django.db.models.signals.`post_delete`:

```python
receiver_function(sender, instance, using)
```

* django.db.models.signals.`m2m_changed`:

```python
receiver_function(sender, instance, action, reverse, model, pk_set, using)
```

### Request/Response Signals

* django.core.signals.`request_started`:

```python
receiver_function(sender, environ)
```

* django.core.signals.`request_finished`:

```python
receiver_function(sender, environ)
```

* django.core.signals.`got_request_exception`:

```python
receiver_function(sender, request)
```

> 完整列表参考[官方文档](https://docs.djangoproject.com/en/1.9/ref/signals/)

## 参考

* [Django - How to trigger an action when a specific model is saved?](https://stackoverflow.com/questions/15155616/django-how-to-trigger-an-action-when-a-specific-model-is-saved)
* [How to Create Django Signals](https://simpleisbetterthancomplex.com/tutorial/2016/07/28/how-to-create-django-signals.html) - 非常详尽的参考文档，本文根据这篇博文翻译整理


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://huataihuang.gitbook.io/cloud-atlas-draft/develop/python/django/startup/trigger_action_when_specific_model_saved.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
