2017-11-03 49 views
0

我正在嘗試向我的數據庫添加唯一性約束以阻止將重複條目添加到連接表。但是,它似乎並沒有工作。我沒有連接表的模型,所以我沒有添加模型級驗證。數據庫唯一性約束不會停止通過關聯創建重複記錄

這裏是遷移:

class CreateBreedsAndTags < ActiveRecord::Migration[5.1] 
    def change 
    create_table :breeds do |t| 
     t.string :name, unique: true, present: true 
     t.timestamps 
    end 

    create_table :tags do |t| 
     t.string :name, unique: true, present: true 
     t.timestamps 
    end 

    create_join_table :breeds, :tags do |t| 
     t.integer :breed_id 
     t.integer :tag_id 
     t.index [:breed_id, :tag_id], unique: true 
    end 
    end 
end 

的品種和型號標籤都非常簡單,而且他們使用has_and_belongs_to_many因爲我想測試出的關聯。我可以添加-> { distinct }到協會,但我想停止重複創建的第一個地方。

class Breed < ApplicationRecord 
    # Some validations and stuff here 
    has_and_belongs_to_many :tags 
end 

如果我在軌道控制檯中創建品種和標籤。我可以做這樣的事情即使是在連接表數據庫級別唯一約束:

b = Breed.create(name: 'b') 
t = Tag.create(name: 't') 
b << t 
b << t 
b.save! 
b.tags # outputs the same tag multiple times 

編輯:

1)值得一提的是,我發現這個stack overflow其中建議overriting在該<<協會。但是,這並不能解釋爲什麼我的獨特約束失敗。

2)我也發現這個stack overflow它建議一個數據庫級約束,但這不適合我。

EDIT2:

下面是從數據庫中的一些表信息:

​​

,我跑了一個\d breeds_tags

Table "public.breeds_tags" 
    Column | Type | Modifiers 
----------+--------+----------- 
breed_id | bigint | not null 
tag_id | bigint | not null 
Indexes: 
    "index_breeds_tags_on_breed_id" btree (breed_id) 
    "index_breeds_tags_on_tag_id" btree (tag_id) 
+1

你看過數據庫中的連接表通過'psql'(即沒有所有的ActiveRecord的東西)? –

+0

@ muistooshort我用一些數據庫信息更新了我的答案。它看起來像創建了兩個索引,但我沒有看到一個唯一的約束?有als oa'breed_tags'表!我基本上從這個[堆棧溢出]運行查詢(https://stackoverflow.com/questions/2204058/list-columns-with-indexes-in-postgresql) – Dbz

+0

@Dbz:你說得對。 Rails創建了兩個索引;它沒有創建獨特的約束。 [Rails's uniqueness' helper](http://guides.rubyonrails.org/active_record_validations.html#uniqueness)「在對象被保存之前驗證該屬性的值是唯一的,它不會在數據庫中創建唯一性約束。 「 –

回答

2

每個移民應該最多創建或修改一個表。每個遷移應該是對db的原子化和可逆變化。如果在同一個遷移中創建引用相同的表和外鍵,如果嘗試將其顛倒,會發生什麼情況?

# rails g model tags name:string 
class CreateTags < ActiveRecord::Migration[5.1] 
    def change 
    create_table :tags do |t| 
     t.string :name 
     t.timestamps 
    end 
    end 
end 

# rails g model breeds name:string 
class CreateBreeds < ActiveRecord::Migration[5.1] 
    def change 
    create_table :breeds do |t| 
     t.string :name 

     t.timestamps 
    end 
    end 
end 

# rails g migration create_join_table_breeds_tags breeds tags 
class CreateJoinTableBreedsTags < ActiveRecord::Migration[5.1] 
    def change 
    create_join_table :breeds, :tags do |t| 
     t.index [:breed_id, :tag_id], unique: true 
    end 
    end 
end 

此外,create_join_table宏創建外鍵列。所以,你不需要手動添加:

# don't do this. 
t.integer :breed_id 
t.integer :tag_id 

事實上,你應該幾乎從來不使用t.integer的關聯。改用引用宏。

這將創建如預期那樣工作的唯一性約束:

=> #<ActiveRecord::Associations::CollectionProxy [#<Tag id: 1, name: "bar", created_at: "2017-11-03 23:34:51", updated_at: "2017-11-03 23:34:51">]> 
irb(main):005:0> b.tags << t 
    (0.2ms) BEGIN 
    SQL (3.8ms) INSERT INTO "breeds_tags" ("breed_id", "tag_id") VALUES ($1, $2) [["breed_id", 1], ["tag_id", 1]] 
    (0.2ms) ROLLBACK 
ActiveRecord::RecordNotUnique: PG::UniqueViolation: ERROR: duplicate key value violates unique constraint "index_breeds_tags_on_breed_id_and_tag_id" 
DETAIL: Key (breed_id, tag_id)=(1, 1) already exists. 

但是,如果你需要的加入是唯一的,你應該使用has_many through:並創建一個模型has_and_belongs_to不提供一種方式,讓應用程式檢查數據庫驅動程序爆炸之前的唯一性。它會要求你用一些非常骯髒的救援語句來包裝你的代碼來捕獲ActiveRecord::RecordNotUnique異常。

自從exceptions should not be used for normal flow control以後,這不是一個好主意。

# rails g model breed_tag breed:belongs_to 

# the table naming for has_many through: is different 
class CreateBreedTags < ActiveRecord::Migration[5.1] 
    def change 
    create_table :breed_tags do |t| 
     t.belongs_to :breed, foreign_key: true 
     t.belongs_to :tag, foreign_key: true 
     t.index [:breed_id, :tag_id], unique: true 
     t.timestamps 
    end 
    end 
end 

class BreedTag < ApplicationRecord 
    belongs_to :breed 
    belongs_to :tag 
    validates_uniqueness_of :breed_id, scope: :tag_id 
end 

class Breed < ApplicationRecord 
    has_many :breed_tags 
    has_many :tags, through: :breed_tags 
end 

class Tag < ApplicationRecord 
    has_many :breed_tags 
    has_many :breeds, through: :breed_tags 
end 
+0

這並不能完全回答你的代碼爲什麼會創建兩個索引。但是我無法複製這個問題,並且經過測試,實際上可以在Rails 5應用程序中使用。 – max

+0

嗨,謝謝你的回答。我同意使用'references'(並且不需要連接表),但我瘋狂地測試任何東西!我會給你的建議一些想法和測試。再次感謝你 – Dbz