自定义 Django 中的条目唯一性



我有一个包含成分列表的数据库。我想避免此表中的重复条目。我不想使用unique关键字有两个原因:

  • 我的唯一性约束比单纯的=要复杂一些
  • 不想在创建预先存在的成分模型时引发异常,相反,我只想返回该模型,以便我可以编写Ingredient(ingredient_name='tomato')并继续我的一天,而不是将所有这些封装在try子句中。这将使我可以轻松地即时将成分添加到我的食谱表中。

一种解决方案是简单地拥有一个像create_ingredient这样的包装器函数,但我不觉得它特别优雅,更具体地说,它对其他开发人员来说并不健壮,只是忘记使用包装器。因此,相反,我正在玩弄pre_init和post_init信号。

这是我到目前为止所拥有的:

class Ingredient(models.Model):
ingredient_name = models.CharField(max_length=200)
recipes = models.ManyToManyField(Recipe,related_name='ingredients')
def __str__(self):
return self.ingredient_name
class Name(models.Model):
main_name = models.CharField(max_length=200, default=None)
equivalent_name = models.CharField(max_length=200, primary_key=True, default=None)
def _add_ingredient(sender, args, **kwargs):
if 'ingredient_name' not in kwargs['kwargs'] :
return
kwargs['kwargs']['ingredient_name'] = kwargs['kwargs']['ingredient_name'].lower()
# check if equivalent name exists, make this one the main one otherwise
try:
kwargs['kwargs']['ingredient_name'] = Name.objects.filter(
equivalent_name=kwargs['kwargs']['ingredient_name']
)[0].main_name
except IndexError:
name = Name(main_name=kwargs['kwargs']['ingredient_name'],
equivalent_name=kwargs['kwargs']['ingredient_name'])
name.save()
pre_init.connect(_add_ingredient, Ingredient)

目前为止,一切都好。这实际上有效,并将在初始化模型之前在需要时替换ingredient_name。现在我想检查有问题的成分是否已经存在,如果存在,则让初始值设定项返回它。我想我需要使用post_init来执行此操作,但我不知道如何修改正在创建的特定实例。这就是我的意思:

def _finalize_ingredient(sender, instance, **kwargs):
try:
# doesn't work because of python's "pass arguments in python's super unique way of doing things" thing
instance = Ingredient.objects.filter(ingredient_name=instance.ingredient_name)[0]
except IndexError:
pass
post_init.connect(_finalize_ingredient, Ingredient)

正如我所评论的,我不希望这起作用,因为instance = ...实际上并没有修改instance,它只是重新分配变量名称(顺便说一句,如果您尝试运行它,会发生各种可怕的事情我不在乎理解,因为我知道这是完全错误的)。那么我实际上该怎么做呢?我真的希望包装器函数不是这里最干净的选择。我是 OOP 的忠实粉丝,天哪,我想要一个 OOP 解决方案(正如我所说,我认为从长远来看,它会比包装器更强大、更安全)。

我当然意识到我可以向 Recipe 添加一个add_ingredient方法,它将为我完成所有这些工作,但我真的很喜欢在我的Ingredient类中包含所有这些的想法,因为它将保证在任何情况下正确的数据库行为。我也很想知道post_init方法是否/如何使用 方法在给定情况下完全覆盖创建的对象。

顺便说一下,你们中的一些人可能想知道为什么我的Name类中没有将Name表连接到Ingredient表的ForeignKey条目。毕竟,这不就是我的检查在我的_add_ingredient方法中完成的吗?原因之一是,如果我这样做,那么我最终会遇到我在这里试图解决的相同问题:如果我想即时创建一种成分以将其添加到我的配方中,我可以在创建Ingredient对象时简单地创建一个Name对象,但如果它对应于已经在使用的main_name,这将引发异常(而不是简单地返回我需要的对象)。

我相信你正在寻找get_or_create(),它已经是Django中的内置

。你提到:

一种解决方案是简单地拥有一个像create_ingredient这样的包装器函数,但我不觉得它特别优雅,更具体地说,它对其他开发人员来说并不健壮,只是忘记使用包装器。

好吧,反过来看。如果您真的需要创建"重复"成分怎么办?那么有这种可能性就好了。

我想出了一些优雅而强大的东西,因为我认为有可能得到我所追求的东西。我仍然必须定义一个add_ingredient方法,但我仍然拥有我需要的健壮性。我已经做到了,以便它可以推广到任何具有主键的类,并且Name表将包含定义任何表的名称唯一性的信息:

class Name(models.Model):
main_name = models.CharField(max_length=200, default=None)
equivalent_name = models.CharField(max_length=200, primary_key=True, default=None)
def _pre_init_unique_fetcher(sender, args, **kwargs):
pk_name = sender._meta.pk.name
if pk_name not in kwargs['kwargs'] :
return
kwargs['kwargs'][pk_name] = kwargs['kwargs'][pk_name].lower()
# check if equivalent name exists, make this one the main one otherwise
try:
kwargs['kwargs'][pk_name] = Name.objects.filter(
equivalent_name=kwargs['kwargs'][pk_name]
)[0].main_name
except IndexError:
name = Name(main_name=kwargs['kwargs'][pk_name],
equivalent_name=kwargs['kwargs'][pk_name])
name.save()
sender._input_dict = kwargs['kwargs']
def _post_init_unique_fetcher(sender, instance, **kwargs):
pk_name = sender._meta.pk.name
pk_instance = instance.__dict__[pk_name]
filter_dict = {}
filter_dict[pk_name] = pk_instance
try:
post_init.disconnect(_post_init_unique_fetcher,sender)
instance.__dict__ = sender.objects.filter(**filter_dict)[0].__dict__
post_init.connect(_post_init_unique_fetcher, sender)
for key in sender._input_dict:
instance.__dict__[key] = sender._input_dict[key]
del sender._input_dict
except IndexError:
post_init.connect(_post_init_unique_fetcher, sender)
except:
post_init.connect(_post_init_unique_fetcher, sender)
raise
unique_fetch_models = [Ingredient, Recipe, WeekPlan]
for unique_fetch_model in unique_fetch_models :
pre_init.connect(_pre_init_unique_fetcher, unique_fetch_model)
post_init.connect(_post_init_unique_fetcher, unique_fetch_model)

现在,这将使用先前模型的预先存在的数据(而不是默认值)加载任何新模型(如果存在具有相同名称的数据)。我在Recipe类中仍然需要add_ingredient方法的原因是,尽管我可以创建模型并立即保存它,但我无法在不引发异常的情况下调用Ingredient.objects.create()来获取预先存在的成分。这与 Django 如何处理primary_key指定有关:如果您创建模型然后save它,它会假设您只是更新条目(如果它已经存在该键),但是如果您create它,它会尝试添加另一个条目,这与primary_key指定冲突。所以现在我可以做像recipe.add_ingredient(Ingredient(ingredient_name='tomato', vegetarian=True))这样的事情。

最新更新