2015-10-23 59 views
1

我使用SQLite使用Django 1.8.4在我的dev的機器,我有這些模型:Django的unique_together可空ForeignKey的

class ModelA(Model): 
    field_a = CharField(verbose_name='a', max_length=20) 
    field_b = CharField(verbose_name='b', max_length=20) 

    class Meta: 
     unique_together = ('field_a', 'field_b',) 


class ModelB(Model): 
    field_c = CharField(verbose_name='c', max_length=20) 
    field_d = ForeignKey(ModelA, verbose_name='d', null=True, blank=True) 

    class Meta: 
     unique_together = ('field_c', 'field_d',) 

我碰到合適的遷移和Django管理登記他們。所以,使用管理員我已經做了這個測試:

  • 我能夠創建ModelA記錄和Django禁止我創建重複記錄 - 如預期!
  • 我不能夠創建相同的ModelB記錄時field_b不爲空
  • 但是,使用field_d爲空

我的問題是,當我能夠創建相同的ModelB記錄:怎麼辦我將unique_together應用於可空的ForeignKey?

我發現這個問題的最近的答案有5年......我確實認爲Django已經發展並且問題可能不一樣。

+0

的可能的複製([Django的唯一一起約束失敗?] http://stackoverflow.com/questions/17510261/django-unique-together-constraint-failure) – Ivan

+0

@Ivan我也試過這篇文章,但它並沒有幫助我與modelformset。當試圖在相同的formset中創建相同的記錄時,Django不會禁止。用戶仍然能夠創建重複項目 – MatheusJardimB

回答

5

UPDATE:我的答案以前的版本是功能性的,但設計不好,這個考慮了一些評論和其他答案。

在SQL中,NULL不等於NULL。這意味着如果你有兩個對象field_d == None and field_c == "somestring"他們不相等,所以你可以創建兩個對象。

您可以覆蓋Model.clean添加你的檢查:

class ModelB(Model): 
    #... 
    def validate_unique(self, exclude=None): 
     if ModelB.objects.exclude(id=self.id).filter(field_c=self.field_c, \ 
           field_d__isnull=True).exists(): 
      raise ValidationError("Duplicate ModelB") 
     super(ModelB, self).validate_unique(exclude) 

如果你要調用full_cleanvalidate_unique形式之外使用。

雖然要小心處理競賽條件。

+0

Tx @Ivan,但是我應該能夠用'field_c ==「a」,field_d == None'創建兩個ModelB記錄? – MatheusJardimB

+0

@MatheusJardimB是的。 – Ivan

+0

而我該如何改變這種行爲:) – MatheusJardimB

4

@ivan,我不認爲django有一個簡單的方法來管理這種情況。您需要考慮並非始終來自表單的所有創建和更新操作。此外,您應該考慮競爭條件...

而且因爲您不強制在數據庫級別使用此邏輯,所以實際上會有兩倍的記錄,您應該在查詢結果時檢查它。

而關於您的解決方案,它可以很好的形式,但我不認爲保存方法可以引發ValidationError。

如果可能的話,最好把這個邏輯委託給DB。在這種特殊情況下,您可以使用兩個部分索引。有一個關於計算器類似的問題 - Create unique constraint with null columns

所以,你可以創建Django的遷移,添加兩個部分索引到你的數據庫

例子:

# Assume that app name is just `example` 

CREATE_TWO_PARTIAL_INDEX = """ 
    CREATE UNIQUE INDEX model_b_2col_uni_idx ON example_model_b (field_c, field_d) 
    WHERE field_d IS NOT NULL; 

    CREATE UNIQUE INDEX model_b_1col_uni_idx ON example_model_b (field_c) 
    WHERE field_d IS NULL; 
""" 

DROP_TWO_PARTIAL_INDEX = """ 
    DROP INDEX model_b_2col_uni_idx; 
    DROP INDEX model_b_1col_uni_idx; 
""" 


class Migration(migrations.Migration): 

    dependencies = [ 
     ('example', 'PREVEOUS MIGRATION NAME'), 
    ] 

    operations = [ 
     migrations.RunSQL(CREATE_TWO_PARTIAL_INDEX, DROP_TWO_PARTIAL_INDEX) 
    ] 
+0

是的,你是對的,在我的其他答案中,我通常補充說,只有在調用'clean'時它纔會起作用。另一方面,除了數據遷移之外,我通常會盡量避免在模型代碼之外進行數據庫修改。 – Ivan

+0

+1數據完整性約束應儘可能靠近數據,最好的方法是使用數據庫約束。任何Python方法(比如接受的答案)都會有競爭條件問題。 – chukkwagon

+0

這可以用Mysql完成嗎?我的理解是,當使用Mysql創建唯一約束時,沒有像「Where is NULL」那樣的功能 –