How to count great grandchild objects in a Rails app? - ruby-on-rails

can anyone help me count the number of great grandchild records in a Rails app?
For example, I want to do something like the following:
class Country
has_many :states
has_many :cities, :through => :states
has_many :events, :through => :cities
end
class State
belongs_to :country
has_many :cities
has_many :events, :through => :cities
end
class City
has_one :country, :through => state
belongs_to :state
has_many :events
end
class Event
belongs_to :city, :counter_cache => true
has_one :state, :through => city, :counter_cache => true
has_one :country, :through => :state, :counter_cache => true
end
So I want to have access to the number of events for each city, for each state, and for each country.
I have City and State working, but don't seem to be able to get a counter_cache running on the great grandparent Country model.
Have I missed something? Is this possible? Is there a better way to do it?
I'd really appreciate some ideas from the community. Thanks!

Have you watched the counter cache railscasts episode? It might be helpful.
http://railscasts.com/episodes/23-counter-cache-column.
If you simply want to count several levels down, you can chain several statements to get your answer. However, this isn't going to be terribly efficient, because of the multiple DB calls to accomplish this, hence it would be better to cache the count, if you're going to be running this count often.
Here's an example of getting the count of all events in a country (untested), something like:
country = Country.find(params[:id])
number_of_events_in_country = 0
country.states.each{|s| s.cities.each{|c| number_of_events_in_country += c.events.count}}

If it's a grandparent relationship you can just use has_many through (as you have listed above), but you have a great grandparent relationship and this doesn't work for that.
One thing you could do (if you have a multiple levels of parent child relationships) is put a method in your Country class to parse it out.
class Country
has_many :states
has_many :cities, :through => :states
attr_accessor :events
def initialize
#events = Array.new
end
def get_events
self.states.each{|s| s.each{|c| c.each{|e| #events << e }}}
end
end
Then just call the get_events method and events will be populated with all the events associated to the first record.
usa = Country.first
usa.get_events
usa.events

I've been trying to figure this out. Just clicked on Jeff's answer's RailsCasts link and found this 6 yr old comment.
Basically, you can do what you want using the counter_culture gem's multi-level caching features by passing it an array of the parents, walking back up to the top. Obviously, be sure to add the corresponding columns as normal.
class Event
belongs_to :city
has_one :state, :through => city
has_one :country, :through => :state
counter_culture [:city, :state, :country]
end

Related

How to establish associations for a model with two belongs_to relationships?

I am building an application with the following model functions
Groups have many Users
Groups have many Expenses (each expense has a :name, :total, :added_by_user_id fields)
Expenses have many owings (1 for each user in the group)
Owings have an :amount and a :user_id, to reference which user the owing is referring
So far, I have set up the models as followings:
# user.rb
class User < ActiveRecord::Base
attr_accessible :first_name, :last_name, :email, :password
has_many :memberships, :foreign_key => "member_id", :dependent => :destroy
has_many :groups, :through => :memberships
has_many :owings
end
# group.rb
class Group < ActiveRecord::Base
attr_accessible :name
has_many :memberships, :dependent => :destroy
has_many :members, :through => :memberships
has_many :expenses
end
# expense.rb
class Expense < ActiveRecord::Base
attr_accessible :total_dollars, :name, :owings_attributes, :added_by_user_id
belongs_to :group, :inverse_of => :expense
has_many :owings, :dependent => :destroy
end
# owing.rb
class Owing < ActiveRecord::Base
attr_accessible :amount_dollars, :user_id
belongs_to :expense, :inverse_of => :owings
belongs_to :user, :inverse_of => :owings
end
# NB - have left off memberships class (and some attributes) for simplicity
To create an expense, I'm using #group.expenses.build(params[:expenses]), where params come from a nested model form that includes attributes for the owings that need to be created. The params include the 'user_id' for each of the 'owing' instances for that expense.
I have two concerns:
Firstly - I've made 'user_id' accessible in the owings model, meaning that a malicious user can change who owes what in an expense (I think?). I don't know how to get around this, though, because the user needs to see the names of all the other members of the group when they fill out the expense/owings form.
Secondly - I've also made 'added_by_user_id' accessible in the expense model - I also wouldn't want malicious users to be able to change this, since this user_id has special edit/delete priveleges for the expense. Is there some clever way to make an expense 'belong_to' a User AND a group, and set both of these associations when creating WITHOUT having to make either an accessible attribute? If it helps, the 'added_by_user_id' can always be set to the current_user.
Any ideas? Very possible I'm missing something fairly fundamental here.
Thanks in advance!
PS. Long time listener, first time caller. Thanks to all of you for teaching me ruby on rails to date; this website is an incredible resource!
Have you thought about setting them dynamically?
dynamic attr-accessible railscast

Rails Associations Through Multiple Levels

I am relatively new to Rails and am working on a project that involves multiple, "nested" levels of data. I am having issues making the proper associations so I can get all the child elements of a model 3 levels higher. Here is an example of the models:
class Country < ActiveRecord::Base
has_many :states
end
class State < ActiveRecord::Base
belongs_to :country
has_many :cities
end
class City < ActiveRecord::Base
belongs_to :state
has_many :people
end
class Person < ActiveRecord::Base
belongs_to :city
end
I have implemented a relationship in the Country model, has_many :cities, :through => :states and have tried to call Country.first.cities.all, which works. However, I am having issues accessing all the people in a given country when I try Country.first.cities.all.people.all in the People controller.
What is the best way to deal with this sort of association situation? Should I add a foreign key to each of the children tables, such as country_id, so that I can get all the People in a Country? Any suggestions would be appreciated.
The reason is that Country.first.cities.all is an array, and each of it's elements has the method people, instead of the entire collection of cities. You'll notice that this works:
Country.first.cities.first.people.all
Because the first city of the first country has the people method. To get a list of all people in a country, you could do the following in a single query:
People.joins(:city => {:state => :country})
.where(:country => {:id => Country.first.id}).all
It's beacouse
Country.first.cities.all
is a collection of cities and it doesn't have people method.
You should go with
Country.first.cities.all.each do |city|
city.people
end

acts_as_list with has_and_belongs_to_many relationship

I've found an old plugin called acts_as_habtm_list - but it's for Rails 1.0.0.
Is this functionality built in acts_as_list now? I can't seem to find any information on it.
Basically, I have an artists_events table - no model. The relationship is handled through those two models specifying :has_and_belongs_to_many
How can I specify order in this situation?
I'm assuming that you have two models - Artist and Event.
You want to have an habtm relationship between them and you want to be able to define an order of events for each artist.
Here's my solution. I'm writing this code from my head, but similar solution works in my case. I'm pretty sure there is a room for improvement.
I'm using rails acts_as_list plugin.
That's how I would define models:
class Artist < ActiveRecord::Base
has_many :artist_events
has_many :events, :through => :artist_events, :order => 'artist_events.position'
end
class Event < ActiveRecord::Base
has_many :artist_events
has_many :artists, :through => :artist_events, :order => 'artist_events.position'
end
class ArtistEvent < ActiveRecord::Base
default_scope :order => 'position'
belongs_to :artist
belongs_to :event
acts_as_list :scope => :artist
end
As you see you need an additional model ArtistEvent, joining the other two. The artist_events table should have two foreign ids and additional column - position.
Now you can use acts_as_list methods (on ArtistEvent model, unfortunately) but something like
Artist.find(:id).events
should give you a list of events belonging to specific artist in correct order.
Additional update for the accepted answer: for Rails 4 and Rails 5:
has_many :events, -> { order 'artist_events.position ASC' }, through: :artist_events
has_many :artists, -> { order 'artist_events.position ASC' }, through: :artist_events
I trying with self-referencing like that
class Product < ActiveRecord::Base
has_many :cross_sales
has_many :cross_sales_products, :through => :cross_sales, :order => 'cross_sales.position'
end
class CrossSale < ActiveRecord::Base
default_scope :order => 'cross_sales.position'
belongs_to :product
belongs_to :cross_sales_product, :class_name => "Product"
acts_as_list :scope => :product
end
create_table :cross_sales, :force => true, :id => false do |t|
t.integer :product_id, :cross_sales_product_id, :position
end
But the field cross_sales.position is never updated ...
An idea ?
Update: Ok the field 'id' it's necessary in the case of additional model with has_many :through option. It's work well now
In the accepted answer, note that :order => 'artist_events.position' is referencing the table artist_events and not the model.
I ran into this minor hiccup when moving from a habtm association to has_many :through.

has_many :through issue with new records

I'm modeling "featuring" based on my plan in this question and have hit a bit of a stumbling block.
I'm defining a Song's primary and featured artists like this:
has_many :primary_artists, :through => :performances, :source => :artist,
:conditions => "performances.role = 'primary'"
has_many :featured_artists, :through => :performances, :source => :artist,
:conditions => "performances.role = 'featured'"
This works fine, except when I'm creating a new song, and I give it a primary_artist via new_song.performances.build(:artist => some_artist, :role => 'primary'), new_song.primary_artists doesn't work (since the performance I created isn't yet saved in the database).
What's the best approach here? I'm thinking of going with something like:
has_many :artists, :through => :performances
def primary_artists
performances.find_all{|p| p.role == 'primary'}.map(&:artist)
end
I think you're overcomplicating it. Just because things have similarities doesn't mean you should put them all in the same box.
class Song < ActiveRecord::Base
has_one :artist # This is your 'primary' artist
has_and_belongs_to_many :featured_artists, :source => :artist # And here you make a featured_artists_songs table for the simple HABTM join
validates_presence_of :artist
end
Poof, no more confusion. You still have to add song.artist before you can save, but that's what you wanted. Right?
There's not much that you can do about the association not being recognized until you save. Arguably, it doesn't really exist until you save, validations pass, and the relevant transaction(s) are completed.
Regarding your question of cleaning up your primary_artist method, you could model it something like this.
class Song < ActiveRecord::Base
has_many :performances
has_many :artists, :through => :performances
has_one :primary_artist, :through => :performances, :conditions => ["performances.roll = ?", "primary"], :source => :artist
end
It's unclear if you want one or many primary artists, but you can easily switch that has_one to has_many as needed.
You've nailed the source of your problem with build vs. create.
As for finding the primary artist of a song. I would add a named_scope on artist to select only featured/primary artists.
class Artist < ActiveRecord::Base
...
named\_scope :primary, :joins => :performances, :conditions => "performances.role = primary"
named\_scope :featured, :joins => :performances, :conditions => "performances.role = featured"
end
To get the primary artist for a song, you would do #song.artists.primary or if you prefer your primary_artists method in song.
def primary_artists
artists.primary
end
However, after looking at your initial question, I think your database layout is insufficient. It's workable but not clear, I've posted my suggestions there, where it belongs.
These named scopes I would also work under my proposed scheme as well.

Use ActiveRecord to Find Result of 5 Nested Tables

I have a User Model(:name, :password, :email), and Event model(:name, :etc) and Interest model (:name)
Then I created two join tables -> UsersInterests and EventsInterests; each not containing a primary key and only comprised of the user_id/interest_id and event_id/interest_id respectively.
I'm trying to use ActiveRecord to query a list all the events where the interest.id of EventsInterests= interest.id of UsersInterests
I'm using has_many and belongs_to relationships with the Nested Loop Plugin
My models look like so =>
user.rb
has_many :users_interests
has_many :interests, :through => :users_interests
event.rb
has_many :events_interests
has_many :interests, :through => :events_interests
interest.rb
belongs_to :users , :through => :users_interests
belongs_to :events , :through => :events_interests
users_interests.rb
belongs_to :users
belongs_to :interests
events_interests.rb
belongs_to :interests
belongs_to :events
If the #user= User.find(1), How would I query the events a user would be interested in?
I came up with this =>
#events.find(:all, :conditions => EventsInterests.interest_id = UsersInterests.interest_id) ??
but I get the error
undefined method `interest_id' for UsersInterests(user_id: integer, interest_id: integer):Class
umm..wtf? any help guys....I've been at this for like 4 days
First, hop into the console and make sure all of your relationships work:
User.first.events
User.first.interests
Events.first.users
Interests.first.users
Interests.first.events
# ... and so
Just to clarify, a User lists his Interests, and you want to get a list of the Events matching those interests?
User.first.interests.collect { |interest| interest.events }.uniq
Not particular efficient, but effective and easy to comprehend.
You could use User.first.interests_singular_ids to get the ids and pass that to a find() with interest_id IN(...) that list, too. I'm not sure how much faster it would be, though.

Resources