2017-11-03 48 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