rails 4 many to many through pivot table - ruby-on-rails

My models:
class Entrant < ActiveRecord::Base
has_many :events, :through => :event_maps
has_many :event_maps, :foreign_key => "entrant_id"
accepts_nested_attributes_for :events, :reject_if => :all_blank
end
class Event < ActiveRecord::Base
has_many :event_maps, :foreign_key => "event_id"
has_many :entrants, :through => :event_maps
accepts_nested_attributes_for :entrants, :reject_if => :all_blank
end
class EventMap < ActiveRecord::Base
belongs_to :event, foreign_key: "event_id"
belongs_to :entrant, foreign_key: "entrant_id"
end
My mappings are correct as far as I can tell, on the console I can do following:
create a new event and add a new entrant:
#event = Event.new(name: 'my event');
#event.save
#event.entrants_attributes = [{name: 'Jack'}]
#event.save
create a new entrant and add a new event:
#entrant = Entrant.new(name: 'Peter')
#entrant.save
#entrant.events_attributes = [{name: 'Great concert'}]
#entrant.save
Now how would I map Peter to my event or Jack to Great concert?
Meaning
I want to register an existing Entrant to an existing Event,
Add a new Entrant to an existing Event or vice versa.
As I said, the many 2 many seems to work both ways, but adding data to the pivot table on existing objects is not really clear to me. Thanks for the help.
Edit: ok I got 2.
#event.entrants.new(name: "hello") #adds a new Entrant works

You can do (as j03w suggested)
#event.entrants << #entrant
The << is the Binary Left Shift Operator which
Binary Left Shift Operator. The left operands value is moved left by
the number of bits specified by the right operand.
It's also used for arrays to push the given object on to the end of this array. This expression returns the array itself, so several appends may be chained together.
$: [] << 'a'
-> ['a']

Related

How to find records, whose has_many through objects include all objects of some list?

I got a typical tag and whatever-object relation: say
class Tag < ActiveRecord::Base
attr_accessible :name
has_many :tagazations
has_many :projects, :through => :tagazations
end
class Tagazation < ActiveRecord::Base
belongs_to :project
belongs_to :tag
validates :tag_id, :uniqueness => { :scope => :project_id }
end
class Project < ActiveRecord::Base
has_many :tagazations
has_many :tags, :through => :tagazations
end
nothing special here: each project is tagged by one or multiple tags.
The app has a feature of search: you can select the certain tags and my app should show you all projects which tagged with ALL mentioned tags. So I got an array of the necessary tag_ids and then got stuck with such easy problem
To do this in one query you'd want to take advantage of the common double not exists SQL query, which essentially does find X for all Y.
In your instance, you might do:
class Project < ActiveRecord::Base
def with_tags(tag_ids)
where("NOT EXISTS (SELECT * FROM tags
WHERE NOT EXISTS (SELECT * FROM tagazations
WHERE tagazations.tag_id = tags.id
AND tagazations.project_id = projects.id)
AND tags.id IN (?))", tag_ids)
end
end
Alternatively, you can use count, group and having, although I suspect the first version is quicker but feel free to benchmark:
def with_tags(tag_ids)
joins(:tags).select('projects.*, count(tags.id) as tag_count')
.where(tags: { id: tag_ids }).group('projects.id')
.having('tag_count = ?', tag_ids.size)
end
This would be one way of doing it, although by no means the most efficient:
class Project < ActiveRecord::Base
has_many :tagazations
has_many :tags, :through => :tagazations
def find_with_all_tags(tag_names)
# First find the tags and join with their respective projects
matching_tags = Tag.includes(:projects).where(:name => tag_names)
# Find the intersection of these lists, using the ruby array intersection operator &
matching_tags.reduce([]) {|result, tag| result & tag.projects}
end
end
There may be a couple of typos in there, but you get the idea

Belongs_to based on value of a field

I have a table with entries, and each entries can have different account-types. I'm trying to define and return the account based on the value of cindof
Each account type has one table, account_site and account_page. So a regular belongs_to won't do.
So is there any way to return something like:
belongs_to :account, :class_name => "AccountSite", :foreign_key => "account_id" if cindof = 1
belongs_to :account, :class_name => "AccountPage", :foreign_key => "account_id" if cindof = 2
Have tried to do that in a method allso, but no luck. Really want to have just one accountand not different belongs_to names.
Anyone that can figure out what I want? Hard to explain in English.
Terw
You should be able to do what you want with a polymorphic association. This won't switch on cindof by default, but that may not be a problem.
class ObjectWithAccount < ActiveRecord::Base
belongs_to :account, :polymorphic => true
end
class AccountSite < ActiveRecord::Base
has_many :objects_with_accounts,
:as => :account,
:class_name => 'ObjectWithAccount'
end
class AccountPage < ActiveRecord::Base
has_many :objects_with_accounts,
:as => :account,
:class_name => 'ObjectWithAccount'
end
You will need both an account_id column and a account_type column. The type of the account object is then stored in the extra type column.
This will let you do:
obj.account = AccountPage.new
or
obj.account = AccountSite.new
I would look into Single Table Inheritance. Not 100% sure, but I think it would solve your problem http://code.alexreisner.com/articles/single-table-inheritance-in-rails.html
If that isn't good, this isn't too hard to implement yourself.
def account
case self.cindof
when 1 then AccountSite.find self.account_id
when 2 then AccountPage.find self.account_id
end
end

Updating a 2-dimensional form

Hey guys. I'm new-ish to rails development and have hit a bit of a wall. The application I'm working on is a scheduling solution that requires updating a join model, but not in a simple 1:1 sort of way.
The app is laid out as follows:
class Route < ActiveRecord::Base
has_many :markers, :foreign_key => 'source_id'
has_many :schedules
accepts_nested_attributes_for :markers, :allow_destroy => true, :reject_if => proc { |a| a['name'].blank? }
accepts_nested_attributes_for :schedules, :allow_destroy => true, :reject_if => proc { |a| a['name'].blank? }
end
class Schedule < ActiveRecord::Base
has_many :arrivals
has_many :markers, :through => :arrivals
accepts_nested_attributes_for :arrivals, :allow_destroy => true, :reject_if => :all_blank
end
class Marker < ActiveRecord::Base
has_many :arrivals
has_many :schedules, :through => :arrivals
end
class Arrival < ActiveRecord::Base
belongs_to :marker
belongs_to :schedule
end
... so a basic has_many :through ... or so I would think :P
When you create a route, you can create 1..n schedules, and 1..n markers. Editing a schedule should allow you to add 1..n arrival entries for each marker defined in the route. THIS is what's causing me grief.
Through the magic of ascii-art, this is what I want the app to look like:
/views/routes/edit.html.erb (works already)
ROUTE
-----
...
SCHEDULES
---------
[Add]
* Schedule 1 [Edit][Delete]
* Schedule 2 [Edit][Delete]
...
MARKERS
-------
[Add]
* Marker 1 [Edit][Delete]
* Marker 2 [Edit][Delete]
* Marker 3 [Edit][Delete]
* Marker 4 [Edit][Delete]
...
/views/schedules/edit.html.erb
SCHEDULE X
----------
[Add Col.]
Marker 1 [ ] [ ]
Marker 2 [ ] [ ]
Marker 3 [ ] [ ]
Marker 4 [ ] [ ]
[x] [x]
(the [x] should remove a column)
EDIT (09NOV04):
I've removed the incomplete view code I originally had posted, but would like to update the question a bit.
I think part of the confusion here (for myself, and possibly for anyone who might be able to help) is that I haven't explained the relationships properly.
markers have many arrivals
schedules have many markers
routes have many schedules
That's the basics.
Having a form that would update arrivals for a single marker wouldn't be difficult, as that's a basic form. What I'm hoping to do is to provide a form that updates all markers at the same time.
When you click on "Add Entry", it should add a new arrival for each marker that's currently available. Under each "column", there should be a "remove" button, that will remove each arrival for that particular column (so from each marker).
I'm not sure if that clears it up any :P
When you create a route, you can
create 1..n schedules, and 1..n
markers. Editing a schedule should
allow you to add 1..n arrival entries
for each marker defined in the route.
THIS is what's causing me grief.
There is nothing linking markers to routes as far as schedules are concerned. The way you've laid things out any schedule can add any number arrival entries for each marker defined in your database.
You need make a few changes to get the functionality you want. I'm assuming that a schedule belongs_to a route. I've also left out the accepts_nested_attributes_for lines out, to conserve space.
class Route < ActiveRecord::Base
has_many :markers, :foreign_key => 'source_id'
has_many :schedules
...
end
class Schedule < ActiveRecord::Base
has_many :arrivals
belongs_to :route
has_many :markers, :through => :arrivals
has_many :route_markers, :through => :route, :source => :markers
...
end
class Marker < ActiveRecord::Base
has_many :arrivals
has_many :schedules, :through => :arrivals
...
end
class Arrival < ActiveRecord::Base
belongs_to :marker
belongs_to :schedule
...
end
Now #schedule.route_markers returns a list of markers in the route linked to the schedule.
You can use those to generate your grid. Then create arrival objects to establish a marker in a specific schedule.
Then it's just a matter of #schedule.markers= list_of_markers and rails takes care of creating/deleting entries in the join table.
Sorry but without knowing more, I'm not going to speculate on what the view will look like.

Rails model with foreign_key and link table

I am trying to create a model for a ruby on rails project that builds relationships between different words. Think of it as a dictionary where the "Links" between two words shows that they can be used synonymously. My DB looks something like this:
Words
----
id
Links
-----
id
word1_id
word2_id
How do I create a relationship between two words, using the link-table. I've tried to create the model but was not sure how to get the link-table into play:
class Word < ActiveRecord::Base
has_many :synonyms, :class_name => 'Word', :foreign_key => 'word1_id'
end
In general, if your association has suffixes such as 1 and 2, it's not set up properly. Try this for the Word model:
class Word < ActiveRecord::Base
has_many :links, :dependent => :destroy
has_many :synonyms, :through => :links
end
Link model:
class Link < ActiveRecord::Base
belongs_to :word
belongs_to :synonym, :class_name => 'Word'
# Creates the complementary link automatically - this means all synonymous
# relationships are represented in #word.synonyms
def after_save_on_create
if find_complement.nil?
Link.new(:word => synonym, :synonym => word).save
end
end
# Deletes the complementary link automatically.
def after_destroy
if complement = find_complement
complement.destroy
end
end
protected
def find_complement
Link.find(:first, :conditions =>
["word_id = ? and synonym_id = ?", synonym.id, word.id])
end
end
Tables:
Words
----
id
Links
-----
id
word_id
synonym_id
Hmm, this is a tricky one. That is because synonyms can be from either the word1 id or the word2 id or both.
Anyway, when using a Model for the link table, you must use the :through option on the Models that use the Link Table
class Word < ActiveRecord::Base
has_many :links1, :class_name => 'Link', :foreign_key => 'word1_id'
has_many :synonyms1, :through => :links1, :source => :word
has_many :links2, :class_name => 'Link', :foreign_key => 'word2_id'
has_many :synonyms2, :through => :links2, :source => :word
end
That should do it, but now you must check two places to get all the synonyms. I would add a method that joined these, inside class Word.
def synonyms
return synonyms1 || synonyms2
end
||ing the results together will join the arrays and eliminate duplicates between them.
*This code is untested.
Word model:
class Word < ActiveRecord::Base
has_many :links, :dependent => :destroy
has_many :synonyms, :through => :links
def link_to(word)
synonyms << word
word.synonyms << self
end
end
Setting :dependent => :destroy on the has_many :links will remove all the links associated with that word before destroying the word record.
Link Model:
class Link < ActiveRecord::Base
belongs_to :word
belongs_to :synonym, :class_name => "Word"
end
Assuming you're using the latest Rails, you won't have to specify the foreign key for the belongs_to :synonym. If I recall correctly, this was introduced as a standard in Rails 2.
Word table:
name
Link table:
word_id
synonym_id
To link an existing word as a synonym to another word:
word = Word.find_by_name("feline")
word.link_to(Word.find_by_name("cat"))
To create a new word as a synonym to another word:
word = Word.find_by_name("canine")
word.link_to(Word.create(:name => "dog"))
I'd view it from a different angle; since all the words are synonymous, you shouldn't promote any one of them to be the "best". Try something like this:
class Concept < ActiveRecord::Base
has_many :words
end
class Word < ActiveRecord::Base
belongs_to :concept
validates_presence_of :text
validates_uniqueness_of :text, :scope => :concept_id
# A sophisticated association would be better than this.
def synonyms
concept.words - [self]
end
end
Now you can do
word = Word.find_by_text("epiphany")
word.synonyms
Trying to implement Sarah's solution I came across 2 issues:
Firstly, the solution doesn't work when wanting to assign synonyms by doing
word.synonyms << s1 or word.synonyms = [s1,s2]
Also deleting synonyms indirectly doesn't work properly. This is because Rails doesn't trigger the after_save_on_create and after_destroy callbacks when it automatically creates or deletes the Link records. At least not in Rails 2.3.5 where I tried it on.
This can be fixed by using :after_add and :after_remove callbacks in the Word model:
has_many :synonyms, :through => :links,
:after_add => :after_add_synonym,
:after_remove => :after_remove_synonym
Where the callbacks are Sarah's methods, slightly adjusted:
def after_add_synonym synonym
if find_synonym_complement(synonym).nil?
Link.new(:word => synonym, :synonym => self).save
end
end
def after_remove_synonym synonym
if complement = find_synonym_complement(synonym)
complement.destroy
end
end
protected
def find_synonym_complement synonym
Link.find(:first, :conditions => ["word_id = ? and synonym_id = ?", synonym.id, self.id])
end
The second issue of Sarah's solution is that synonyms that other words already have when linked together with a new word are not added to the new word and vice versa.
Here is a small modification that fixes this problem and ensures that all synonyms of a group are always linked to all other synonyms in that group:
def after_add_synonym synonym
for other_synonym in self.synonyms
synonym.synonyms << other_synonym if other_synonym != synonym and !synonym.synonyms.include?(other_synonym)
end
if find_synonym_complement(synonym).nil?
Link.new(:word => synonym, :synonym => self).save
end
end

how to access rails join model attributes when using has_many :through

I have a data model something like this:
# columns include collection_item_id, collection_id, item_id, position, etc
class CollectionItem < ActiveRecord::Base
self.primary_key = 'collection_item_id'
belongs_to :collection
belongs_to :item
end
class Item < ActiveRecord::Base
has_many :collection_items
has_many :collections, :through => :collection_items, :source => :collection
end
class Collection < ActiveRecord::Base
has_many :collection_items, :order => :position
has_many :items, :through => :collection_items, :source => :item, :order => :position
end
An Item can appear in multiple collections and also more than once in the same collection at different positions.
I'm trying to create a helper method that creates a menu containing every item in every collection. I want to use the collection_item_id to keep track of the currently selected item between requests, but I can't access any attributes of the join model via the Item class.
def helper_method( collection_id )
colls = Collection.find :all
colls.each do |coll|
coll.items.each do |item|
# !!! FAILS HERE ( undefined method `collection_item_id' )
do_something_with( item.collection_item_id )
end
end
end
I tried this as well but it also fails with ( undefined method `collection_item' )
do_something_with( item.collection_item.collection_item_id )
Edit: thanks to serioys sam for pointing out that the above is obviously wrong
I have also tried to access other attributes in the join model, like this:
do_something_with( item.position )
and:
do_something_with( item.collection_item.position )
Edit: thanks to serioys sam for pointing out that the above is obviously wrong
but they also fail.
Can anyone advise me how to proceed with this?
Edit: -------------------->
I found from online documentation that using has_and_belongs_to_many will attach the join table attributes to the retreived items, but apparently it is deprecated. I haven't tried it yet.
Currently I am working on amending my Collection model like this:
class Collection < ActiveRecord::Base
has_many :collection_items, :order => :position, :include => :item
...
end
and changing the helper to use coll.collection_items instead of coll.items
Edit: -------------------->
I've changed my helper to work as above and it works fine - (thankyou sam)
It's made a mess of my code - because of other factors not detailed here - but nothing that an hour or two of re-factoring wont sort out.
In your example you have defined in Item model relationship as has_many for collection_items and collections the generated association method is collection_items and collections respectively both of them returns an array so the way you are trying to access here is wrong. this is primarily case of mant to many relationship. just check this Asscociation Documentation for further reference.
do_something_with( item.collection_item_id )
This fails because item does not have a collection_item_id member.
do_something_with( item.collection_item.collection_item_id )
This fails because item does not have a collection_item member.
Remember that the relation between item and collection_items is a has_many. So item has collection_items, not just a single item. Also, each collection has a list of collection items. What you want to do is probably this:
colls = Collection.find :all
colls.each do |coll|
coll.collection_items.each do |collection_item|
do_something_with( collection_item.id )
end
end
A couple of other pieces of advice:
Have you read the documentation for has_many :through in the Rails Guides? It is pretty good.
You shouldn't need the :source parameters in the has_many declarations, since you have named your models and associations in a sensible way.
I found from online documentation that using has_and_belongs_to_many will attach the join table attributes to the retreived items, but apparently it is deprecated. I haven't tried it yet.
I recommend you stick with has_many :through, because has_and_belongs_to_many is more confusing and doesn't offer any real benefits.
I was able to get this working for one of my models:
class Group < ActiveRecord::Base
has_many :users, :through => :memberships, :source => :user do
def with_join
proxy_target.map do |user|
proxy_owner = proxy_owner()
user.metaclass.send(:define_method, :membership) do
memberships.detect {|_| _.group == proxy_owner}
end
user
end
end
end
end
In your case, something like this should work (haven't tested):
class Collection < ActiveRecord::Base
has_many :collection_items, :order => :position
has_many :items, :through => :collection_items, :source => :item, :order => :position do
def with_join
proxy_target.map do |items|
proxy_owner = proxy_owner()
item.metaclass.send(:define_method, :join) do
collection_items.detect {|_| _.collection == proxy_owner}
end
item
end
end
end
end
Now you should be able to access the CollectionItem from an Item as long as you access your items like this (items.with_join):
def helper_method( collection_id )
colls = Collection.find :all
colls.each do |coll|
coll.items.with_join.each do |item|
do_something_with( item.join.collection_item_id )
end
end
end
Here is a more general solution that you can use to add this behavior to any has_many :through association:
http://github.com/TylerRick/has_many_through_with_join_model
class Collection < ActiveRecord::Base
has_many :collection_items, :order => :position
has_many :items, :through => :collection_items, :source => :item, :order => :position, :extend => WithJoinModel
end

Resources