Using Rails 3.2, what's wrong with this code?
#reviews = #user.reviews.includes(:user, :reviewable)
.where('reviewable_type = ? AND reviewable.shop_type = ?', 'Shop', 'cafe')
It raises this error:
Can not eagerly load the polymorphic association :reviewable
If I remove the reviewable.shop_type = ? condition, it works.
How can I filter based on the reviewable_type and reviewable.shop_type (which is actually shop.shop_type)?
My guess is that your models look like this:
class User < ActiveRecord::Base
has_many :reviews
end
class Review < ActiveRecord::Base
belongs_to :user
belongs_to :reviewable, polymorphic: true
end
class Shop < ActiveRecord::Base
has_many :reviews, as: :reviewable
end
You are unable to do that query for several reasons.
ActiveRecord is unable to build the join without additional information.
There is no table called reviewable
To solve this issue, you need to explicitly define the relationship between Review and Shop.
class Review < ActiveRecord::Base
belongs_to :user
belongs_to :reviewable, polymorphic: true
# For Rails < 4
belongs_to :shop, foreign_key: 'reviewable_id', conditions: "reviews.reviewable_type = 'Shop'"
# For Rails >= 4
belongs_to :shop, -> { where(reviews: {reviewable_type: 'Shop'}) }, foreign_key: 'reviewable_id'
# Ensure review.shop returns nil unless review.reviewable_type == "Shop"
def shop
return unless reviewable_type == "Shop"
super
end
end
Then you can query like this:
Review.includes(:shop).where(shops: {shop_type: 'cafe'})
Notice that the table name is shops and not reviewable. There should not be a table called reviewable in the database.
I believe this to be easier and more flexible than explicitly defining the join between Review and Shop since it allows you to eager load in addition to querying by related fields.
The reason that this is necessary is that ActiveRecord cannot build a join based on reviewable alone, since multiple tables represent the other end of the join, and SQL, as far as I know, does not allow you join a table named by the value stored in a column. By defining the extra relationship belongs_to :shop, you are giving ActiveRecord the information it needs to complete the join.
If you get an ActiveRecord::EagerLoadPolymorphicError, it's because includes decided to call eager_load when polymorphic associations are only supported by preload. It's in the documentation here: http://api.rubyonrails.org/v5.1/classes/ActiveRecord/EagerLoadPolymorphicError.html
So always use preload for polymorphic associations. There is one caveat for this: you cannot query the polymorphic assocition in where clauses (which makes sense, since the polymorphic association represents multiple tables.)
This did the work for me
belongs_to :shop, foreign_type: 'Shop', foreign_key: 'reviewable_id'
Not enough reputation to comment to extend the response from Moses Lucas above, I had to make a small tweak to get it to work in Rails 7 as I was receiving the following error:
ArgumentError: Unknown key: :foreign_type. Valid keys are: :class_name, :anonymous_class, :primary_key, :foreign_key, :dependent, :validate, :inverse_of, :strict_loading, :autosave, :required, :touch, :polymorphic, :counter_cache, :optional, :default
Instead of belongs_to :shop, foreign_type: 'Shop', foreign_key: 'reviewable_id'
I went with belongs_to :shop, class_name: 'Shop', foreign_key: 'reviewable_id'
The only difference here is changing foreign_type: to class_name:!
As an addendum the answer at the top, which is excellent, you can also specify :include on the association if for some reason the query you are using is not including the model's table and you are getting undefined table errors.
Like so:
belongs_to :shop,
foreign_key: 'reviewable_id',
conditions: "reviews.reviewable_type = 'Shop'",
include: :reviews
Without the :include option, if you merely access the association review.shop in the example above, you will get an UndefinedTable error ( tested in Rails 3, not 4 ) because the association will do SELECT FROM shops WHERE shop.id = 1 AND ( reviews.review_type = 'Shop' ).
The :include option will force a JOIN instead. :)
#reviews = #user.reviews.includes(:user, :reviewable)
.where('reviewable_type = ? AND reviewable.shop_type = ?', 'Shop', 'cafe').references(:reviewable)
When you are using SQL fragments with WHERE, references is necessary to join your association.
Related
I have a Totem model and Totems table.
There will be many totems and I need to store the order of the totems in the database table.
I added a previous_totem_id and next_totem_id to the Totems table to store the order information. I did it via this
Rails Migration:
class AddPreviousNextTotemColumnsToTotems < ActiveRecord::Migration
def change
add_column :totems, :previous_totem_id, :integer
add_column :totems, :next_totem_id, :integer
end
end
Now in the Model I have defined the relationships:
class Totem < ActiveRecord::Base
validates :name, :presence => true
has_one :previous_totem, :class_name => 'Totem'
has_one :next_totem, :class_name => 'Totem'
end
I created a couple of these totems through ActiveRecord and tried to use the previous_totem_id column like so:
totem = Totem.create! name: 'a1'
Totem.create! name: '1a'
totem.previous_totem_id = Totem.find_by(name: '1a').id
puts totem.previous_totem #This is NIL
However, the previous_totem comes back as nil, and I do not see a select statement in the mysql log when calling this line
totem.previous_totem
Is this relationship recommended? What is the best way to implement a self referencing column?
Changing direction of the association from has_one to belongs_to and specifying foreign keys, should make your code work as you expected:
class Totem < ActiveRecord::Base
validates :name, :presence => true
belongs_to :previous_totem, :class_name => 'Totem', foreign_key: :previous_totem_id
belongs_to :next_totem, :class_name => 'Totem', foreign_key: :next_totem_id
end
However, good association should be properly named and declared on both sides - with matching has_one association; in this case it's impossible without naming conflicts :) Self join might be sometimes useful, but i'm not sure if it's the best solution here. I didn't use the gem moveson recommends, but an integer column to store position is something I use and IMHO makes reordering records easier :)
If the only reason for the self-reference is to store the order of the totems, please don't do it this way. It's your lucky day: This is a solved problem!
Use a position field and the acts_as_list gem, which will take care of this problem for you in a neat and performant way.
I have a few models...
class Game < ActiveRecord::Base
belongs_to :manager, class_name: 'User'
has_many :votes
end
class Vote < ActiveRecord::Base
belongs_to :game
belongs_to :voter, class_name: 'User'
end
class User < ActiveRecord::Base
has_many :games, dependent: :destroy
has_many :votes, dependent: :destroy
end
In my controller, I have the following code...
user = User.find(params[:userId])
games = Game.includes(:manager, :votes)
I would like to add an attribute/method voted_on_by_user to game that takes a user_id parameter and returns true/false. I'm relatively new to Rails and Ruby in general so I haven't been able to come up with a clean way of accomplishing this. Ideally I'd like to avoid the N+1 queries problem of just adding something like this on my Game model...
def voted_on_by_user(user)
votes.where(voter: user).exists?
end
but I'm not savvy enough with Ruby/Rails to figure out a way to do it with just one database roundtrip. Any suggestions?
Some things I've tried/researched
Specifying conditions on Eager Loaded Associations
I'm not sure how to specify this or give the includes a different name like voted_on_by_user. This doesn't give me what I want...
Game.includes(:manager, :votes).includes(:votes).where(votes: {voter: user})
Getting clever with joins. So maybe something like...
Game.includes(:manager, :votes).joins("as voted_on_by_user LEFT OUTER JOIN votes ON votes.voter_id = #{userId}")
Since you are already includeing votes, you can just count votes using non-db operations: game.votes.select{|vote| vote.user_id == user_id}.present? does not perform any additional queries if votes is preloaded.
If you necessarily want to put the field in the query, you might try to do a LEFT JOIN and a GROUP BY in a very similar vein to your second idea (though you omitted game_id from the joins):
Game.includes(:manager, :votes).joins("LEFT OUTER JOIN votes ON votes.voter_id = #{userId} AND votes.game_id = games.id").group("games.id").select("games.*, count(votes.id) > 0 as voted_on_by_user")
I have a legacy application not written in Ruby. The legacy app uses a SQL Server DB and my Rails app needs to access this legacy DB.
The database has a User table, ViewKey table, and two bridge tables that implement two many-to-many relationships between User and ViewKey. The bridge tables are UserViewKeyAllowed and UserViewKeyDenied. So each user may have multiple view keys they are allowed to access and multiple view keys they are denied access.
Here are the models I have defined:
class User < ActiveRecord::Base
self.table_name = 'tblUser'
attribute_names.each { |attr| alias_attribute attr.underscore, attr }
has_many :user_groups, foreign_key: :tblUserID
has_many :user_view_key_alloweds, foreign_key: :tblUserID
has_many :user_view_key_denieds, foreign_key: :tblUserID
has_many :groups, through: :user_groups
has_many :view_key_alloweds, through: :user_view_key_alloweds, source: :view_key
has_many :view_key_denieds, through: :user_view_key_denieds, source: :view_key
end
class ViewKey < ActiveRecord::Base
self.table_name = 'tblViewKey'
attribute_names.each { |attr| alias_attribute attr.underscore, attr }
has_many :group_view_keys, foreign_key: :tblViewKeyID
has_many :groups, through: :group_view_keys
has_many :user_view_key_alloweds, foreign_key: 'tblViewKeyID'
has_many :user_view_key_denieds, foreign_key: 'tblUserID'
end
class UserViewKeyDenied < ActiveRecord::Base
self.table_name = 'tblUserViewKeyDenied'
attribute_names.each { |attr| alias_attribute attr.underscore, attr }
belongs_to :view_key, primary_key: 'ID', foreign_key: 'tblViewKeyID'
belongs_to :user, primary_key: 'ID', foreign_key: 'tblViewKeyID'
end
class UserViewKeyAllowed < ActiveRecord::Base
self.table_name = 'tblUserViewKeyAllowed'
attribute_names.each { |attr| alias_attribute attr.underscore, attr }
belongs_to :view_key, primary_key: 'ID', foreign_key: 'tblViewKeyID'
belongs_to :user, primary_key: 'ID', foreign_key: 'tblUserID'
end
I've got the "has_many: ... through:" association working and the following Ruby seems to query the data correctly:
user = User.where(ID: 116).eager_load(:view_key_alloweds, :view_key_denieds).first
I'd like to be able to do something like the following:
user = User.where(ID: 116).eager_load(:view_key_alloweds, :view_key_denieds).where('tblViewKey.IsWebView = 1').first
I need to expand the second "where" in be something like "tblViewKey.IsWebView = 1 AND SomeAlias.IsWebView = 1". Is there a way to specify the alias for the ViewKey model or is there a better way to do this? Does active record have a standard way of naming table aliases?
I'm looking into using a scope but I'm not sure how to define one for a "has_many ... through" association.
After more research I was able to come up with a solution that works and has good performance. Active Record does have a scheme for creating table aliases when joining to the same table multiple times. It seems to be the association name plus the name of the table joining from. So using the models shown I can query the data I want using:
#user = User.where(ID: params[:user_id])
.eager_load(:view_key_alloweds, :view_key_denieds, groups: [:view_keys])
.where('[tblViewKey].IsWebView = 1 OR [view_key_denieds_tblUser].IsWebView = 1 OR [view_keys_tblGroup].IsWebView = 1')
.first
The :view_key_alloweds association is listed first in the call to eager_load so my where does not need a alias for that join to the tblViewKey table. The :view_key_denieds also joins to the tblViewKey table but since it joins from the tblUser table I know the alias will be [view_key_denieds_tblUser]. The association through "groups: [:view_key]" is accomplished through the tblGroups table so the alias will be [view_keys_tblGroup].
My code works but I don't know of a way to tap into the aliases being generated. A future version of Rails and Active Record could break my code. There is a discussion here https://github.com/rails/rails/issues/12224 about aliases but I'm new enough to ruby, rails, and active record that most of it was over my head.
Setup
For this question, I'll use the following three classes:
class SolarSystem < ActiveRecord::Base
has_many :planets
scope :has_earthlike_planet, joins(:planets).merge(Planet.like_earth)
end
class Planet < ActiveRecord::Base
belongs_to :solar_system
belongs_to :planet_type
scope :like_earth, joins(:planet_type).where(:planet_types => {:life => true, :gravity => 9.8})
end
class PlanetType < ActiveRecord::Base
has_many :planets
attr_accessible :gravity, :life
end
Problem
The scope has_earthlike_planet does not work. It gives me the following error:
ActiveRecord::ConfigurationError: Association named 'planet_type' was
not found; perhaps you misspelled it?
Question
I have found out that this is because it is equivalent to the following:
joins(:planets, :planet_type)...
and SolarSystem does not have a planet_type association. I'd like to use the like_earth scope on Planet, the has_earthlike_planet on SolarSystem, and would like to avoid duplicating code and conditions. Is there a way to merge these scopes like I'm attempting to do but am missing a piece? If not, what other techniques can I use to accomplish these goals?
Apparently, at this time you can only merge simple constructs that don't involve joins. Here is a possible workaround if you modify your models to look like this:
class SolarSystem < ActiveRecord::Base
has_many :planets
has_many :planet_types, :through => :planets
scope :has_earthlike_planet, joins(:planet_types).merge(PlanetType.like_earth)
end
class Planet < ActiveRecord::Base
belongs_to :solar_system
belongs_to :planet_type
scope :like_earth, joins(:planet_type).merge(PlanetType.like_earth)
end
class PlanetType < ActiveRecord::Base
has_many :planets
attr_accessible :gravity, :life
scope :like_earth, where(:life => true, :gravity => 9.8)
end
** UPDATE **
For the record, a bug was filed about this behavior - hopefully will be fixed soon...
You are reusing the conditions from the scope Planet.like_earth, which joins planet_type. When these conditions are merged, the planet_type association is being called on SolarSystem, which doesn't exist.
A SolarSystem has many planet_types through planets, but this is still not the right association name, since it is pluralized. You can add the following to the SolarSystem class to setup the planet_type association, which is just an alias for planet_types. You can't use the Ruby alias however since AREL reflects on the association macros, and doesn't query on whether the model responds to a method by that name:
class SolarSystem < ActiveRecord::Base
has_many :planets
has_many :planet_types, :through => :planets
has_many :planet_type, :through => :planets, :class_name => 'PlanetType'
scope :has_earthlike_planet, joins(:planets).merge(Planet.like_earth)
end
SolarSystem.has_earthlike_planet.to_sql # => SELECT "solar_systems".* FROM "solar_systems" INNER JOIN "planets" ON "planets"."solar_system_id" = "solar_systems"."id" INNER JOIN "planets" "planet_types_solar_systems_join" ON "solar_systems"."id" = "planet_types_solar_systems_join"."solar_system_id" INNER JOIN "planet_types" ON "planet_types"."id" = "planet_types_solar_systems_join"."planet_type_id" WHERE "planet_types"."life" = 't' AND "planet_types"."gravity" = 9.8
An easy solution that I found is that you can change your joins in your Planet class to
joins(Planet.joins(:planet_type).join_sql)
This will create an SQL string for the joins which will always include the correct table names and therefore should always be working no matter if you call the scope directly or use it in a merge. It's not that nice looking and may be a bit of a hack, but it's only a little more code and there's no need to change your associations.
I have 3 models: Question, Option, Rule
Question has_many options;
Option needs a foreign key for question_id
Rule table consists of 3 foreign_keys:
2 columns/references to question_ids -> foreign keys named as 'assumption_question_id' and 'consequent_question_id'
1 column/reference to option_id -> foreign key named as option_id or condition_id
Associations for Rule:
Question has_many rules; and
Option has_one rule
I want to understand how to write up migrations for this, and how that associates to the 'has_many'/'belongs_to' statements I write up in my model, and the ':foreign_key' option I can include in my model.
I had this for my Option migration, but I'm not sure how the "add_index" statement works in terms of foreign keys, and how I can use it for my Rule migration: (my Question and Options models have appropriate has_many and belongs_to statements - and work fine)
class CreateOptions < ActiveRecord::Migration
def change
create_table :options do |t|
t.integer :question_id
t.string :name
t.integer :order
t.timestamps
end
add_index :options, :question_id
end
end
Thank you for the help!
Note: I have found this way to solve the problem.Kindness from China.
If you have RailsAdmin with you,you may notice that you can see all rules of one question as long as one field of both question fields(assumption_question_id,consequent_question_id) equals to id of the question.
I have done detailed test on this and found out that Rails always generates a condition "question_id = [current_id]" which make to_sql outputs
SELECT `rules`.* FROM `rules` WHERE `rules`.`question_id` = 170
And the reason that the following model
class Question < ActiveRecord::Base
has_many :options
# Notice ↓
has_many :rules, ->(question) { where("assumption_question_id = ? OR consequent_question_id = ?", question.id, question.id) }, class_name: 'Rule'
# Notice ↑
end
makes Question.take.rules.to_sql be like this
SELECT `rules`.* FROM `rules` WHERE `rules`.`question_id` = 170 AND (assumption_question_id = 170 OR consequent_question_id = 170)
Is that we have not yet get ride of the annoy question_id so no matter how we describe or condition properly, our condition follows that "AND".
Then,we need to get ride of it.How?
Click here and you will know how,Find sector 8.1,and you can see
Article.where(id: 10, trashed: false).unscope(where: :id)
# SELECT "articles".* FROM "articles" WHERE trashed = 0
Then lets do it:
class Question < ActiveRecord::Base
has_many :options
# Notice ↓
has_many :rules, ->(question) { unscope(where: :question_id).where("assumption_question_id = ? OR consequent_question_id = ?", question.id, question.id) }, class_name: 'Rule'
# Notice ↑
end
class Rule < ActiveRecord::Base
belongs_to :option
belongs_to :assumption_question, class_name: "Question", foreign_key: :assumption_question_id, inverse_of: :assumption_rules
belongs_to :consequent_question, class_name: "Question", foreign_key: :consequent_question_id, inverse_of: :consequent_rules
end
class Option < ActiveRecord::Base
belongs_to :question
has_one :rule
end
All done.
Finally
This is my first answer here at stackoverflow,and this method is never found anywhere else.
Thanks for reading.
add_index adds an index to column specified, nothing more.
Rails does not provide native support in migrations for managing foreign keys. Such functionality is included in gems like foreigner. Read the documentation that gem to learn how it's used.
As for the associations, just add the columns you mentioned in your Question to each table (the migration you provided looks fine; maybe it's missing a :rule_id?)
Then specify the associations in your models. To get you started
class Question < ActiveRecord::Base
has_many :options
has_many :assumption_rules, class_name: "Rule"
has_many :consequent_rules, class_name: "Rule"
end
class Rule < ActiveRecord::Base
belongs_to :option
belongs_to :assumption_question, class_name: "Question", foreign_key: :assumption_question_id, inverse_of: :assumption_rules
belongs_to :consequent_question, class_name: "Question", foreign_key: :consequent_question_id, inverse_of: :consequent_rules
end
class Option < ActiveRecord::Base
belongs_to :question
has_one :rule
end
Note This is just a (untested) start; options may be missing.
I strongly recommend you read
http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html
http://guides.rubyonrails.org/association_basics.html
Edit: To answer the question in your comment
class Option < ActiveRecord::Base
belongs_to :question
# ...
The belongs_to tells rails that the question_id column in your options table stores an id value for a record in your questions table. Rails guesses the name of the column is question_id based on the :question symbol. You could instruct rails to look at a different column in the options table by specifying an option like foreign_key: :question_reference_identifier if that was the name of the column. (Note your Rule class in my code above uses the foreign_key option in this way).
Your migrations are nothing more than instructions which Rails will read and perform commands on your database based from. Your models' associations (has_many, belongs_to, etc...) inform Rails as to how you would like Active Record to work with your data, providing you with a clear and simple way to interact with your data. Models and migrations never interact with one another; they both independently interact with your database.
You can set a foreign key in your model like this:
class Leaf < ActiveRecord::Base
belongs_to :tree, :foreign_key => "leaf_code"
end
You do not need to specify this in a migration, rails will pull the foreign key from the model class definition.