come evitare i duplicati in un has_many: attraverso una relazione?
-
11-07-2019 - |
Domanda
Come posso ottenere quanto segue? Ho due modelli (blog e lettori) e una tabella JOIN che mi permetterà di avere una relazione N: M tra di loro:
class Blog < ActiveRecord::Base
has_many :blogs_readers, :dependent => :destroy
has_many :readers, :through => :blogs_readers
end
class Reader < ActiveRecord::Base
has_many :blogs_readers, :dependent => :destroy
has_many :blogs, :through => :blogs_readers
end
class BlogsReaders < ActiveRecord::Base
belongs_to :blog
belongs_to :reader
end
Quello che voglio fare ora è aggiungere lettori a blog diversi. La condizione, tuttavia, è che posso solo aggiungere un lettore a un blog UNA VOLTA. Quindi non ci devono essere duplicati (stesso readerID
, stesso blogID
) nella tabella BlogsReaders
. Come posso raggiungere questo obiettivo?
La seconda domanda è: come posso ottenere un elenco di blog a cui i lettori non sono già abbonati (ad es. per compilare un elenco di selezione a discesa, che può quindi essere utilizzato per aggiungere il lettore a un altro blog)?
Soluzione
Che dire di:
Blog.find(:all,
:conditions => ['id NOT IN (?)', the_reader.blog_ids])
Rails si occupa della raccolta di ID per noi con metodi di associazione! :)
http://api.rubyonrails.org/classes/ActiveRecord/Associations/ ClassMethods.html
Altri suggerimenti
Soluzione più semplice integrata in Rails:
class Blog < ActiveRecord::Base
has_many :blogs_readers, :dependent => :destroy
has_many :readers, :through => :blogs_readers, :uniq => true
end
class Reader < ActiveRecord::Base
has_many :blogs_readers, :dependent => :destroy
has_many :blogs, :through => :blogs_readers, :uniq => true
end
class BlogsReaders < ActiveRecord::Base
belongs_to :blog
belongs_to :reader
end
Nota aggiungendo l'opzione :uniq => true
alla has_many
chiamata.
Inoltre, potresti prendere in considerazione has_and_belongs_to_many
tra Blog e Reader, a meno che tu non abbia alcuni altri attributi che vorresti avere sul modello di join (cosa che al momento no). Questo metodo ha anche un :uniq
opiton.
Nota che questo non ti impedisce di creare le voci nella tabella, ma ti assicura che quando esegui una query sulla raccolta ottieni solo uno di ogni oggetto.
Aggiorna
In Rails 4 il modo per farlo è tramite un blocco dell'ambito. Quanto sopra diventa.
class Blog < ActiveRecord::Base
has_many :blogs_readers, dependent: :destroy
has_many :readers, -> { uniq }, through: :blogs_readers
end
class Reader < ActiveRecord::Base
has_many :blogs_readers, dependent: :destroy
has_many :blogs, -> { uniq }, through: :blogs_readers
end
class BlogsReaders < ActiveRecord::Base
belongs_to :blog
belongs_to :reader
end
Aggiornamento per Rails 5
L'uso di uniq
nel blocco dell'ambito provocherà un errore NoMethodError: undefined method 'extensions' for []:Array
. Usa invece distinct
:
class Blog < ActiveRecord::Base
has_many :blogs_readers, dependent: :destroy
has_many :readers, -> { distinct }, through: :blogs_readers
end
class Reader < ActiveRecord::Base
has_many :blogs_readers, dependent: :destroy
has_many :blogs, -> { distinct }, through: :blogs_readers
end
class BlogsReaders < ActiveRecord::Base
belongs_to :blog
belongs_to :reader
end
Questo dovrebbe occuparsi della tua prima domanda:
class BlogsReaders < ActiveRecord::Base
belongs_to :blog
belongs_to :reader
validates_uniqueness_of :reader_id, :scope => :blog_id
end
La via di Rails 5.1
class Blog < ActiveRecord::Base
has_many :blogs_readers, dependent: :destroy
has_many :readers, -> { distinct }, through: :blogs_readers
end
class Reader < ActiveRecord::Base
has_many :blogs_readers, dependent: :destroy
has_many :blogs, -> { distinct }, through: :blogs_readers
end
class BlogsReaders < ActiveRecord::Base
belongs_to :blog
belongs_to :reader
end
La risposta a questo link mostra come sovrascrivere " < < " metodo per ottenere ciò che stai cercando senza sollevare eccezioni o creare un metodo separato: Rails idioma per evitare duplicati in has_many: attraverso
Penso che qualcuno troverà una risposta migliore di questa.
the_reader = Reader.find(:first, :include => :blogs)
Blog.find(:all,
:conditions => ['id NOT IN (?)', the_reader.blogs.map(&:id)])
[modifica]
Vedi la risposta di Josh qui sotto. È la strada da percorrere. (Sapevo che c'era una via migliore là fuori;)
La risposta principale attualmente dice di usare uniq
nel proc:
class Blog < ActiveRecord::Base
has_many :blogs_readers, dependent: :destroy
has_many :readers, -> { uniq }, through: :blogs_readers
end
Questo però dà il via alla relazione in un array e può spezzare cose che si aspettano di eseguire operazioni su una relazione, non su un array.
Se si utilizza distinct
lo mantiene come una relazione:
class Blog < ActiveRecord::Base
has_many :blogs_readers, dependent: :destroy
has_many :readers, -> { distinct }, through: :blogs_readers
end
Il modo più semplice è serializzare la relazione in un array:
class Blog < ActiveRecord::Base
has_many :blogs_readers, :dependent => :destroy
has_many :readers, :through => :blogs_readers
serialize :reader_ids, Array
end
Quindi, quando si assegnano valori ai lettori, li si applica come
blog.reader_ids = [1,2,3,4]
Quando si assegnano le relazioni in questo modo, i duplicati vengono rimossi automaticamente.