Let's imagine that I have a CPA tracking system.
I would have following models: an Offer, it has some Landings, each of them has multiple Links, each of the links has a bunch of Visits.
So, I what I want is DRY code, therefore offer_id column within visits table is unacceptable. The workaround here is delegated methods like this:
class Offer < ActiveRecord::Base
has_many :landings
has_many :links, through: :landings
has_many :visits, through: :landings
end
class Landing < ActiveRecord::Base
belongs_to :offer
has_many :links
has_many :visits, through: :links
end
class Link < ActiveRecord::Base
belongs_to :landing
has_many :visits
delegate :offer, to: :landing
end
class Visit < ActiveRecord::Base
belongs_to :link
delegate :landing, to: :link
delegate :offer, to: :link
end
It works nice with a single visit, e.g. visit.offer.id. But what if I need different visits associated with one offer?
The issue is that I'm unable to construct a valid query using ActiveRecord API. It might look like Visits.where(offer: Offer.first), but it doesn't work this way, saying ActiveRecord::StatementInvalid: SQLite3::SQLException: no such column: visits.offer: SELECT "visits".* FROM "visits" WHERE "visits"."offer" = 1, which is predictable.
Question: How should I organize my code to make statements like Visits.where(offer: Offer.first) work efficiently without duplicating offer_id column within visits table?
You code was organized nicely, don't need to refactor I think. You can achieve that by defining a scope in Visit like this:
class Visit < ActiveRecord::Base
scope :from_offer, -> (offer) {
joins(link: :landing).where(ladings: {offer_id: offer.id})
}
scope :from_landing, -> (landing) {
joins(:link).where(links: {landing_id: landing.id})
}
end
So the query will be:
Visit.from_offer(Offer.first)
Related
I'm having some trouble trying to fetch some models via SQL in rails and I was wondering if anyone knows of a good solution for this particular problem. Basically, these are what my classes look like:
class SubscriberList < ActiveRecord::Base
has_many :subscriptions
end
class Subscription < ActiveRecord::Base
has_many :messages
belongs_to :subscription_list
end
class Announcement < ActiveRecord::Base
has_many :messages
end
class Message < ActiveRecord::Base
belongs_to :subscription
belongs_to :announcement
end
Now, I want to access all the Announcements for a SubscriptionList excluding duplicates. Is this possible? Can it be done with a single SQL query or is that just wishful thinking?
For example here's what I want:
class SubscriptionList < ActiveRecord::Base
def announcements
Announcements.joins(:messages).where(message: { subscription: {subscription_list: self} })
end
end
I think your idea is correct in general. Try this variant
Announcements.
joins(messages: {subscription: :subscription_list}).
where(subscription_lists: {id: self.id})
# opposite
SubscriptionList.
joins(subscriptions: {messages: :announcement}).
where(announcements: {id: self.id})
Notes:
* these queries may return duplicates - so uniq can be added to them
* self can be omitted (I wrote it to show that this is id of instance and avoid missunderstanding)
In my Rails 4 app, I have the following models:
class Person < ActiveRecord::Base
has_many :addresses
end
class Address < ActiveRecord::Base
belongs_to :person
belongs_to :city
end
class City < ActiveRecord::Base
has_many :addresses
end
I'm using the :includes function to return query result into one variable:
Address.includes(:person, :city).where("person_id = 1")
It works as expected, except that I do not want the query to return every single column.
Here's what I've tried:
use select and specify table name and column names explicitly, e.g. "city.name", but Rails generates a big query with outer joins, that can be very costly, especially when there are lots of concurrent requests, so prefer a better solution.
don't want to hard code complete and raw SQL statements, because of maintenance issue later on
create a new "dummy" belongs_to relationship like in Address: belongs_to :city_select_columns, -> { select('name') }, :class => 'City', but that doesn't work (actually I'm not sure if that select is even supported, only came across documentation about where so far).
maybe define scope in City and Person? but I'm not sure how it should be defined or if it'd make sense to do it this way
Suggestions? Thanks
Have you tried this?
class Person < ActiveRecord::Base
has_many :addresses
has_many :cities, :through => :addresses
end
class Address < ActiveRecord::Base
belongs_to :person
belongs_to :city
end
class City < ActiveRecord::Base
has_many :addresses
end
Then:
Person.find(1).cities.pluck(:name)
Looks like this generates an INNER JOIN but with indexes it shouldn't be too costly?
Did you try select?
Address.select(<output_columns>).includes(:person, :city).where("person_id = 1")
Could not find a good query method using Rails' API, I ended up writing a raw inner join SQL, then call ActiveRecord::Base.connection.execute to run it.
I'm adding quiz functionality to the twitter app from the Hartl tutorial and have these Models:
User is nearly the same as the tutorial:
class User < ActiveRecord::Base
has_many :followed_users, through: :relationships, source: :followed
has_many :takens, dependent: :destroy
has_many :questions, through: :takens
end
Taken is a table of Question ids to User ids:
class Taken < ActiveRecord::Base
belongs_to :user
belongs_to :question
end
nothing interesting in Question:
class Question < ActiveRecord::Base
attr_accessible :category, :correct, :option1, :option2, :option3, :qn
end
I want to be able to show followed_users and followers in order of the number of tests they have taken. In the console this can be had through:
User.find_by_id(1).question_ids.count
Then I can do something like:
User.find_by_id(1).followers.first.question_ids.count
in the console to get the count for a single follower.
I feel like I'm almost there.
How do I sort the followers and followed_users through their 'takens' count? (I was also looking at cache_count, which at first seemed promising, but might not be what I need...)
Ruby on Rails does not provide an object oriented mechanism to perform this; you have to write the SQL yourself. In your case, I'd say that the following line SHOULD work:
User.find_by_sql("SELECT users.*, COUNT(questions.id)
AS c FROM users, questions WHERE questions.user_id = users.id
GROUP BY users.id ORDER BY c DESC")
I don't have the actual tables in front of me, so I can't be sure that this is actual valid SQL, but hopefully it should work.
EDIT: There were a few syntax errors with my SQL but they've been fixed. Note that I'm assuming that your tables are called users and questions. They may differ for you.
I'm trying to get a grasp of how to work with associations in Rails, specifically, when and when not to write explicit SQL code.
In my application, I have four models, which are defined as follows:
class User < ActiveRecord::Base
has_many :comments
has_many :geographies
has_many :communities, through: :geographies
class Comment < ActiveRecord::Base
belongs_to :user
class Community < ActiveRecord::Base
has_many :geographies
has_many :users
class Geography < ActiveRecord::Base
belongs_to :user
belongs_to :community
Users can post comments, and are associated to one or more communities through the geography table (the geography table stores user_id and community_id).
I have an index action listing all comments, and I would like to filter by community. Given a comment object, I can get the user object via comment.user, but I can't chain beyond that (i.e., something like comment.user.geography(0).community doesn't work).
It seems this object chaining is a key feature of rails, but does it work with has_many :through associations? Given my example, is it possible to get the community object from the comment object by using object chaining, or would I need to write the SQL to get anything other than the user when given the comment object?
Since User is associated with multiple communities, you will need to tell ActiveRecord (or raw SQL) which community you want:
comment.user.communities #=> should give you all the communities
If you don't particularly care for getting all communities and just want to get any community
comment.user.communities.first #=> should give you the first community
But, generally you will be interested in one particular community, based on a condition.
comment.user.communities.where(name: 'Europe') #=> should give you the European community.
I don't think you need the geographies table.
Try
class Community < ActiveRecord::Base
has_many :users
end
class User < ActiveRecord::Base
belongs_to :community
has_many :comments
end
class Comment < ActiveRecord::Base
belongs_to :user
end
Then you can access a comment's user's community like
#comment.user.community
I print in my view a number that tell me, how many people read my article. It looks something like a:
<%=article.hits.count%>
As is possible to see, I created a simple association.
Now I am trying to get the information, if the user who is log in on my page, so if he is already had read this article. In my table that contains hits is column user_id.
But I can't still find the way, how to get...
I tried something like:
<% if session[:login_user_id].hits.user_id == session[:login_user_id]%>
Have you read it already.
<% end %>
But the example above doesn't work me... Could anyone help me please, how to do?
EDIT: The models:
class Article < ActiveRecord::Base
has_many :hits
end
class Hits < ActiveRecord::Base
belongs_to :article, :class_name => "DataHit", :foreign_key => "article_id"
has_many :users
end
class User < ActiveRecord::Base
belongs_to :hit
end
Thanks in advance
Let's first talk about the model you like to receive. For me, it sounds like:
Every article can be visited / read by many users.
Every user can read / visit many articles.
This is a classical n:m-association which is normally implemented by a has-many-through association.
If this is the intention, it should be implemented like:
class Article < ActiveRecord::Base
has_many :hits
has_many :users, :through => :hits
end
class Hits < ActiveRecord::Base
belongs_to :article, :class_name => "DataHit", :foreign_key => "article_id"
belongs_to :user
end
class User < ActiveRecord::Base
has_many :hits
has_many :articles, :through => :hits
end
Of course, you have to add migrations that ensure that the final DB model is like that:
Hit has article_id and user_id to ensure that users may find the articles they have read
If you have that model implemented, it should be more easy. Then you have operations available like: #article.users.contains(User.find(user_id)). Have a look at the tutorial at Ruby on Rails Guides which explain what the has-many-through relation is and which advantages they have.
It would be helpful if you try the things first in the console of Rails. To do that, start with:
Start the rails console in the root directory of your application: rails c
Enter there e.g.: art = Article.find(1) to get the article with the id.
Try which methods are available: art.methods.sort to see all methods that could be used. If there is no method users, you have did something wrong with the assocication.
Try the call: us = art.users and look at the result. It should be a rails specific object, an object that behaves like a collection and understands how to add and remove users to that collection (with the whole life cycle of rails). The error your currently have could mean different things:
Your database model does not match your associations defined in Rails (I suspect that).
Some minor tweak (misspelling somewhere) which hinders Rails.
I hope this gives you some clues what to do next, I don't think that we can fix the problem here once and for all times.