在数据库

时间:2015-09-18 12:58:05

标签: python django database database-design logic

这不是关于存储过程(或者至少我认为不是)。

假设我有一个数据库。数据库中包含一些城市,以及在这些城市中发生的事件。有些人会使用此网站,他们希望在进入网站时收到有关某些事件的通知。
用于指定他们想要通知的事件的规则应该是通用的。

例如,我希望用户能够说“我希望收到有关所有活动的通知,这些活动将在星期日,在1200年到1400年之间成立的城市,在名称开头的国家/地区字母“F”或在南美洲“,将在伪逻辑代码中翻译:

  (
  event.date.day == Sunday 
  and 
  event.city.founded_date.year.between(1200, 1400)
  ) 
AND 
  (
  event.city.country.starts_with("F")
  or
  event.city.country.continent == "South Africa"
  )

“大陆是”,“天是”,“基础日期介于”之间等规则是预先定义的,用户会选择它,但我希望将来能够添加新规则。

存储此类逻辑的最佳方法是什么?我能想出的唯一解决方案是“NotificationGatherer”模型。它将包含用户的id和带有json的字符串。 我会创建一个json二叉树,对于这个特殊情况,大写的“AND”将是一个有两个孩子的根 - 内部和内部或。 第一个孩子将有两个简单的条件,这将反映实际情况为生。

然后我会根据用户的请求调用一个方法,该方法可以:

  1. 评估为所有即将发生的事件设置的条件的值(真/假)

  2. 使用过滤器形成一个查询集,该过滤器将获取满足给定条件的所有即将发生的事件(更加困难且效率更高)。

  3. 现在,这是一个好方法,还是我应该尝试别的东西? 它似乎相当复杂(我已经看到测试它会有多痛苦),我可以想象很多人过去可能需要这样的东西,但我找不到任何建议,因为任何寻找“数据库中的逻辑”自动指向我关于存储过程的文章/问题。

    如果这有任何区别,我正在使用django和mysql。

2 个答案:

答案 0 :(得分:3)

如果是我的话,我会将规则存储在数据库中 使用Celery不时处理它们。

对于模型部分,我认为多表继承是要走的路,因为不同的规则需要存储不同的数据。 在我看来,django-polymorphic是你的朋友:

我建议如下:

from django.db import models
from polymorphic import PolymorphicModel


class AbtractRuleObject(models.Model):
    class Meta:
        abstract = True

    def filter_queryset(self, queryset):
        """Will handle actual filtering of the event queryset"""
        raise NotImplementedError

    def match_instance(self, instance):
        raise NotImplementedError

class RuleSet(AbtractRuleObject):
    """Will manage the painful part o handling the OR / AND logic inside the database"""
    NATURE_CHOICES = (
        ('or', 'OR'),
        ('and', 'AND'),
    )
    nature = models.CharField(max_length=5, choices=NATURE_CHOICES, default='and')

    # since a set can belong to another set, etc.
    parent_set = models.ForeignKey('self', null=True, blank=True, related_name='children')

    def filter_queryset(self, queryset):
        """This is rather naive and could be optimized"""
        if not self.parent_set:
            # this is a root rule set so we just filter according to registered rules
            for rule in self.rules:
                if self.nature == 'and':
                    queryset = rule.filter_queryset(queryset)
                elif self.nature == 'or':
                    queryset = queryset | rule.filter_queryset(queryset)
        else:
            # it has children rules set
            for rule_set in self.children:
                if self.nature == 'and':
                    queryset = rule_set.filter_queryset(queryset)
                elif self.nature == 'or':
                    queryset = queryset | rule_set.filter_queryset(queryset)
        return queryset

    def match_instance(self, instance):
        if not self.parent_set:
            if self.nature == 'and':
                return all([rule_set.match_instance(instance) for rule_set in self.children])
            if self.nature == 'any':
                return any([rule_set.match_instance(instance) for rule_set in self.children])
        else:
            if self.nature == 'and':
                return all([rule_set.match_instance(instance) for rule_set in self.children])
            if self.nature == 'any':
                return any([rule_set.match_instance(instance) for rule_set in self.children])

class Rule(AbtractRuleObject, PolymorphicModel):
    """Base class for all rules"""
    attribute = models.CharField(help_text="Attribute of the model on which the rule will apply")
    rule_set = models.ForeignKey(RuleSet, related_name='rules')

class DateRangeRule(Rule):
    start = models.DateField(null=True, blank=True)
    end = models.DateField(null=True, blank=True)

    def filter_queryset(self, queryset):
        filters = {}
        if self.start:
            filters['{0}__gte'.format(self.attribute)] = self.start
        if self.end:
            filters['{0}__lte'.format(self.attribute)] = self.end
        return queryset.filter(**filters)

    def match_instance(self, instance):
        start_ok = True
        end_ok = True
        if self.start:
            start_ok = getattr(instance, self.attribute) >= self.start
        if self.end:
            end_ok = getattr(instance, self.attribute) <= self.end

        return start_ok and end_ok

class MatchStringRule(Rule):
    match = models.CharField()
    def filter_queryset(self, queryset):
        filters = {'{0}'.format(self.attribute): self.match}
        return queryset.filter(**filters)

    def match_instance(self, instance):
        return getattr(instance, self.attribute) == self.match

class StartsWithRule(Rule):
    start = models.CharField()

    def filter_queryset(self, queryset):
        filters = {'{0}__startswith'.format(self.attribute): self.start}
        return queryset.filter(**filters)

    def match_instance(self, instance):
        return getattr(instance, self.attribute).startswith(self.start)

现在,假设您的EventCity模型如下:

class Country(models.Model):
    continent = models.CharField()
    name = models.CharField(unique=True)

class City(models.Model):
    name = models.CharField(unique=True)
    country = models.ForeignKey(Country)
    founded_date = models.DateField()

class Event(models.Model):
    name = models.CharField(unique=True)
    city = models.ForeignKey(City)
    start = models.DateField()
    end = models.DateField()

然后您可以使用我的示例如下:

global_set = RuleSet(nature='and')
global_set.save()

set1 = RuleSet(nature='and', parent_set=global_set)
set1.save()

year_range = DateRangeRule(start=datetime.date(1200, 1, 1),
                           end=datetime.date(1400, 1, 1),
                           attribute='city__founded_date',
                           rule_set=set1)
year_range.save()

set2 = RuleSet(nature='or', parent_set=global_set)
set2.save()

startswith_f = StartsWithRule(start='F',
                              attribute='city__country__name')
                              rule_set=set2)
startswith_f.save()

exact_match = MatchStringRule(match='South Africa',
                              attribute='city__country__continent')
                              rule_set=set2)
exact_match.save()

queryset = Event.objects.all()

# Magic happens here

# Get all instances corresponding to the rules
filtered_queryset = global_set.filter_queryset(queryset)

# Check if a specific instance match the rules
assert global_set.match_instance(filtered_queryset[0]) == True

代码绝对未经测试,但我认为它最终可以起作用,或者至少可以给你 一个实现的想法。

我希望它有所帮助!

答案 1 :(得分:1)

它不是关于数据库中的逻辑,它更好地称为存储过滤器模式或存储过滤器首选项。

通常,您希望让您的用户能够在配置文件设置中创建和存储过滤器,这些过滤器将从数据库中提取与之匹配的所有事件,并向用户发送有关它们的通知。

首先,您应该考虑过滤器必须有多深。它可以是这样的:

  1. 模型FilterSet - 将具有一些全局设置(例如通知类型)并将分配给特定用户
  2. 模型Filter - 将有一个过滤规则(或一组规则,例如日期范围),并将分配给FilterSet
  3. 每个用户都应该能够定义多个filterset。创建查询时,所有过滤器将与AND连接在一起(过滤器中的某些规则除外。该特定过滤器的类型将设置它)。

    创建某些类型的过滤器(甚至开始的日期范围,星期几等)后,您将过滤器类型存储在一列中,并使用json序列化过滤其他列或一列中的参数。

    当发送通知时,处理器会检查每个FilterSet是否返回一些数据,如果是,它会将返回的数据发送给FilterSet的所有者。

    在json中存储整个WHERE条件并不复杂,但它会提供类似的灵活性。您只需为用户创建多个FilterSet即可覆盖一些复杂的案例。

相关问题