Python Django模型字段根据相关实例自动更新

Python Django模型字段根据相关实例自动更新,python,django,foreign-keys,Python,Django,Foreign Keys,我正在寻找一种能够根据相关模型实例自动更新模型实例的解决方案 看起来很简单,我到处都找过了。我是新手,所以请让我知道我问的问题不对,或者思考的方式不对 我需要Account.monthly_捐款自动更新为过去30天内所有相关捐款的总额。同样,对于年度和终身捐赠 我还需要Account.u订阅是否切换为True如果 每月捐款>=20,或 年度捐款>=200或 终身捐赠>=10000 如果不满足要求,则返回False class Account(models.Model): ...

我正在寻找一种能够根据相关模型实例自动更新模型实例的解决方案

看起来很简单,我到处都找过了。我是新手,所以请让我知道我问的问题不对,或者思考的方式不对

我需要Account.monthly_捐款自动更新为过去30天内所有相关捐款的总额。同样,对于年度和终身捐赠

我还需要Account.u订阅是否切换为True如果

  • 每月捐款>=20,或
  • 年度捐款>=200或
  • 终身捐赠>=10000
如果不满足要求,则返回False

class Account(models.Model):
    ...
    is_subscribed = models.BooleanField(
        default=False
    )
    monthly_donations = models.PositiveIntegerField(
        default=0
    )
    yearly_donations = models.PositiveIntegerField(
        default=0
    )
    lifetime_donations = models.PositiveIntegerField(
        default=0
    )


class Donation(models.Model):
    related_account = models.ForeignKey(
        Account,
        on_delete=models.CASCADE,
        related_name='donation'
    )
    amount = models.IntegerField()
    date = models.DateField()

信号是解决方案吗?但是,我不希望总数仅在保存新捐赠时更新。我需要根据捐赠日期每天更新。信号不能100%解决您的问题。因为
每月捐款
每年捐款
取决于当前日期,才能为您提供正确答案

当代码的多个部分对同一事件感兴趣时,信号通常是一个很好的解决方案(在您的情况下,只有
帐户
实例对新的
捐赠
对象感兴趣)。也可以在您没有直接访问代码时(例如第三方应用程序,或内置模型,如
User
权限
无法修改代码)

在您的情况下,您可以访问
捐赠
模型,因此您可能可以覆盖
save()
方法:

class Donation(models.Model):
    # db fields ...

    def save(self, *args, **kwargs):
        super().save(*args, **kwargs)  # call the actual save method
        update_account_totals()        # execute this code every time the model is saved
但是,正如您已经提到的,您不希望仅在创建新捐赠时更新此信息

实际上,您当前存储在
帐户
模型中的所有信息都可以动态计算。使用更高级的QuerySet表达式,我们可以直接在数据库中完成所有工作。即使我们每次都要计算它,它也可以非常快。特别是在缓存结果时

这种查询称为。我将使用
annotate()
函数为您的用例写下一个示例。
annotate()
函数是一个特殊函数,它根据我们定义的标准向结果集的每个实例“添加额外字段”

通常,我们用
annotate()
添加的字段是某物的和,或计数,或平均值。在您的情况下,我们可以列出所有
账户
对象,对于每个对象,我们可以用每月捐款、每年捐款和所有时间捐款的总和进行注释。我们甚至可以使用case/when查询,使用这个计算字段来检查帐户是否是订户

它比我们一直使用的诸如
filter()
all()
等琐碎的查询集稍微复杂一点,但我花了时间把它写下来,并给出一些可能对您有所帮助的注释

PS:下面的示例使用了Django 2.0上引入的新

实施:

import datetime

from django.utils import timezone
from django.db.models import Case, When, Q, Value, BooleanField, Sum
from django.db.models.functions import Coalesce

# first we use the `timedelta` to get refernce points
# considering now = 2018-02-14, `last_month` will be equal to 2018-01-14
# and `last_year` will be equal to 2017-02-14
last_month = timezone.now() - datetime.timedelta(days=30)
last_year = timezone.now() - datetime.timedelta(days=365)

# here we are building a sub-query using the Sum object, we are going to use it next
# to sum all the donations happened after "2018-01-14", that is, in the last 30 days
monthly_sum_expression = Sum('donation__amount', filter=Q(donation__date__gte=last_month))

# similar to the previous one, we are summing all donations that happened after "2017-02-14"
# that is, one year ago
yearly_sum_expression = Sum('donation__amount', filter=Q(donation__date__gte=last_year))

# here we are not applying any filter, so we will sum *all* donations
all_time_sum_expression = Sum('donation__amount')

# below, we are building the logic to tell if the person is a subscriber or not (based on the criteria you
# entered in your question.. monthly donations >= 20, or yearly donations >= 200, etc)
# The pipe "|" means it's an OR. the "Q" is an object that holds a database expression
# if any of those criteria are met, then it will return "True"
subscriber_condition = When(Q(monthly__gte=20) | Q(yearly__gte=200) | Q(all_time__gte=1000), then=Value(True))
subscriber_expression = Case(subscriber_condition, default=Value(False), output_field=BooleanField())

# now here we build our query. in this case we are selection *all* the accounts and for each account
# we are adding 4 extra fields calculated on the fly: monthly, yearly, all_time, and subscriber_status.
# the Coalesce function is for the cases where the account had no donation in the last 30 days
# instead of returning "None" it will return "0"
accounts = Account.objects.annotate(
    monthly=Coalesce(monthly_sum_expression, Value(0)),
    yearly=Coalesce(yearly_sum_expression, Value(0)),
    all_time=Coalesce(all_time_sum_expression, Value(0)),
    subscriber_status=subscriber_expression
)
# then in your code you can use those calculated fields just like you would if it was a regular database field
for account in accounts:
    print(account.monthly)
    print(account.yearly)
    print(account.all_time)
    print(account.subscriber_status)
用法:

import datetime

from django.utils import timezone
from django.db.models import Case, When, Q, Value, BooleanField, Sum
from django.db.models.functions import Coalesce

# first we use the `timedelta` to get refernce points
# considering now = 2018-02-14, `last_month` will be equal to 2018-01-14
# and `last_year` will be equal to 2017-02-14
last_month = timezone.now() - datetime.timedelta(days=30)
last_year = timezone.now() - datetime.timedelta(days=365)

# here we are building a sub-query using the Sum object, we are going to use it next
# to sum all the donations happened after "2018-01-14", that is, in the last 30 days
monthly_sum_expression = Sum('donation__amount', filter=Q(donation__date__gte=last_month))

# similar to the previous one, we are summing all donations that happened after "2017-02-14"
# that is, one year ago
yearly_sum_expression = Sum('donation__amount', filter=Q(donation__date__gte=last_year))

# here we are not applying any filter, so we will sum *all* donations
all_time_sum_expression = Sum('donation__amount')

# below, we are building the logic to tell if the person is a subscriber or not (based on the criteria you
# entered in your question.. monthly donations >= 20, or yearly donations >= 200, etc)
# The pipe "|" means it's an OR. the "Q" is an object that holds a database expression
# if any of those criteria are met, then it will return "True"
subscriber_condition = When(Q(monthly__gte=20) | Q(yearly__gte=200) | Q(all_time__gte=1000), then=Value(True))
subscriber_expression = Case(subscriber_condition, default=Value(False), output_field=BooleanField())

# now here we build our query. in this case we are selection *all* the accounts and for each account
# we are adding 4 extra fields calculated on the fly: monthly, yearly, all_time, and subscriber_status.
# the Coalesce function is for the cases where the account had no donation in the last 30 days
# instead of returning "None" it will return "0"
accounts = Account.objects.annotate(
    monthly=Coalesce(monthly_sum_expression, Value(0)),
    yearly=Coalesce(yearly_sum_expression, Value(0)),
    all_time=Coalesce(all_time_sum_expression, Value(0)),
    subscriber_status=subscriber_expression
)
# then in your code you can use those calculated fields just like you would if it was a regular database field
for account in accounts:
    print(account.monthly)
    print(account.yearly)
    print(account.all_time)
    print(account.subscriber_status)
这将是一个最佳解决方案。这样,您就可以确保数据始终是最新的。另一个解决方案是保持数据库字段与您的相同,并在服务器上创建一些字段,这样它们将每天更新您的所有帐户,并且您的
帐户
字段将用作计算字段的“缓存”


然后您可以将cron作业与我在开始时提到的
save()
方法覆盖结合起来。

信号并不能100%解决您的问题。因为
每月捐款
每年捐款
取决于当前日期,才能为您提供正确答案

当代码的多个部分对同一事件感兴趣时,信号通常是一个很好的解决方案(在您的情况下,只有
帐户
实例对新的
捐赠
对象感兴趣)。此外,当您无法直接访问代码时(例如,第三方应用程序,或内置模型,如
用户
权限
,您无法对其进行修改)

在您的情况下,您可以访问
捐赠
模型,因此您可能可以覆盖
save()
方法:

class Donation(models.Model):
    # db fields ...

    def save(self, *args, **kwargs):
        super().save(*args, **kwargs)  # call the actual save method
        update_account_totals()        # execute this code every time the model is saved
但是,正如您已经提到的,您不希望仅在创建新捐赠时更新此信息

实际上,您当前存储在
帐户
模型中的所有信息都可以动态计算。使用更高级的QuerySet表达式,我们可以直接在数据库中完成所有工作。即使我们每次都要计算它,它也可以非常快。特别是在缓存结果时

这种查询称为。我将使用
annotate()
函数为您的用例写下一个示例。
annotate()
函数是一个特殊函数,它根据我们定义的标准向结果集的每个实例“添加额外字段”

通常,我们用
annotate()
添加的字段是某物的和,或计数,或平均值。在您的情况下,我们可以列出所有
账户
对象,对于每个对象,我们可以用每月捐款、每年捐款和所有时间捐款的总和进行注释。我们甚至可以使用case/when查询,使用这个计算字段来检查帐户是否是订户

它比我们一直使用的诸如
filter()
all()
等琐碎的查询集稍微复杂一点,但我花了时间把它写下来,并给出一些可能对您有所帮助的注释

PS:下面的示例使用了Django 2.0上引入的新

实施:

import datetime

from django.utils import timezone
from django.db.models import Case, When, Q, Value, BooleanField, Sum
from django.db.models.functions import Coalesce

# first we use the `timedelta` to get refernce points
# considering now = 2018-02-14, `last_month` will be equal to 2018-01-14
# and `last_year` will be equal to 2017-02-14
last_month = timezone.now() - datetime.timedelta(days=30)
last_year = timezone.now() - datetime.timedelta(days=365)

# here we are building a sub-query using the Sum object, we are going to use it next
# to sum all the donations happened after "2018-01-14", that is, in the last 30 days
monthly_sum_expression = Sum('donation__amount', filter=Q(donation__date__gte=last_month))

# similar to the previous one, we are summing all donations that happened after "2017-02-14"
# that is, one year ago
yearly_sum_expression = Sum('donation__amount', filter=Q(donation__date__gte=last_year))

# here we are not applying any filter, so we will sum *all* donations
all_time_sum_expression = Sum('donation__amount')

# below, we are building the logic to tell if the person is a subscriber or not (based on the criteria you
# entered in your question.. monthly donations >= 20, or yearly donations >= 200, etc)
# The pipe "|" means it's an OR. the "Q" is an object that holds a database expression
# if any of those criteria are met, then it will return "True"
subscriber_condition = When(Q(monthly__gte=20) | Q(yearly__gte=200) | Q(all_time__gte=1000), then=Value(True))
subscriber_expression = Case(subscriber_condition, default=Value(False), output_field=BooleanField())

# now here we build our query. in this case we are selection *all* the accounts and for each account
# we are adding 4 extra fields calculated on the fly: monthly, yearly, all_time, and subscriber_status.
# the Coalesce function is for the cases where the account had no donation in the last 30 days
# instead of returning "None" it will return "0"
accounts = Account.objects.annotate(
    monthly=Coalesce(monthly_sum_expression, Value(0)),
    yearly=Coalesce(yearly_sum_expression, Value(0)),
    all_time=Coalesce(all_time_sum_expression, Value(0)),
    subscriber_status=subscriber_expression
)
# then in your code you can use those calculated fields just like you would if it was a regular database field
for account in accounts:
    print(account.monthly)
    print(account.yearly)
    print(account.all_time)
    print(account.subscriber_status)
用法:

import datetime

from django.utils import timezone
from django.db.models import Case, When, Q, Value, BooleanField, Sum
from django.db.models.functions import Coalesce

# first we use the `timedelta` to get refernce points
# considering now = 2018-02-14, `last_month` will be equal to 2018-01-14
# and `last_year` will be equal to 2017-02-14
last_month = timezone.now() - datetime.timedelta(days=30)
last_year = timezone.now() - datetime.timedelta(days=365)

# here we are building a sub-query using the Sum object, we are going to use it next
# to sum all the donations happened after "2018-01-14", that is, in the last 30 days
monthly_sum_expression = Sum('donation__amount', filter=Q(donation__date__gte=last_month))

# similar to the previous one, we are summing all donations that happened after "2017-02-14"
# that is, one year ago
yearly_sum_expression = Sum('donation__amount', filter=Q(donation__date__gte=last_year))

# here we are not applying any filter, so we will sum *all* donations
all_time_sum_expression = Sum('donation__amount')

# below, we are building the logic to tell if the person is a subscriber or not (based on the criteria you
# entered in your question.. monthly donations >= 20, or yearly donations >= 200, etc)
# The pipe "|" means it's an OR. the "Q" is an object that holds a database expression
# if any of those criteria are met, then it will return "True"
subscriber_condition = When(Q(monthly__gte=20) | Q(yearly__gte=200) | Q(all_time__gte=1000), then=Value(True))
subscriber_expression = Case(subscriber_condition, default=Value(False), output_field=BooleanField())

# now here we build our query. in this case we are selection *all* the accounts and for each account
# we are adding 4 extra fields calculated on the fly: monthly, yearly, all_time, and subscriber_status.
# the Coalesce function is for the cases where the account had no donation in the last 30 days
# instead of returning "None" it will return "0"
accounts = Account.objects.annotate(
    monthly=Coalesce(monthly_sum_expression, Value(0)),
    yearly=Coalesce(yearly_sum_expression, Value(0)),
    all_time=Coalesce(all_time_sum_expression, Value(0)),
    subscriber_status=subscriber_expression
)
# then in your code you can use those calculated fields just like you would if it was a regular database field
for account in accounts:
    print(account.monthly)
    print(account.yearly)
    print(account.all_time)
    print(account.subscriber_status)
这将是一个巨大的挑战