Multiple associations to same table with alias - ruby-on-rails

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.

Related

Rails belongs_to association with multiple foreign keys

I'm trying to figure out a way to define a belongs_to association where the target record can be found by one of 4 different columns. The setup:
User
class User < ActiveRecord::Base
has_many :managerships, foreign_key: :employee_id
has_many :managers, through: :managerships
end
Managership
class Managership < ActiveRecord::Base
belongs_to :employee, class_name: 'User'
belongs_to :manager # <-- ideas?
end
The managerships table has 4 columns manager_id, manager_custom_id, manager_email, manager_saml_id that we can use to look up the user's managers. For every Managership, only one of these columns is ever present.
I'm looking for a way to be able to call user.managers (a manager is a User) so that it returns all users where managerships.manager_id = users.id OR managerships.manager_custom_id = users.custom_id OR managerships.manager_email = users.email OR managerships.manager_saml_id = users.saml_id
Many thanks for any ideas!
Although, I would doubt that 4 foreign keys for one relation is a good idea at all, one way to get managers from User is to use method instead of scope. However you won't be able to use #joins, if you do so.
class User
def managers
self.class.joins(:managerships).where('managerships.manager_id = ? OR managerships.manager_custom_id = ? OR managerships.manager_email = ? OR managerships.manager_saml_id', id, custom_id, emd, saml_id)
end
end
in Managership you can do similar thing, declare #manger instead of belongs_to :manager

Rails ActiveRecord getter override

So I have a model called Primer which has_many :amplicons. However, in my DB the Amplicons table stores the primer id in either a forward_primer_id or reverse_primer_id column (i.e. not the default primer_id convension). In the Primer model I would like to direct Rails to search the Amplicons table in both of those columns for the primer_id and associate those amplicons with that primer. I think I can write a method in the Primer model to do this but is there a more Rails way?
Thanks in advance!
OPTION 1
class Primer
has_many :forward_amplicons, class_name: "Amplicon", foreign_key: :forward_primer_id
has_many :reverse_amplicons, class_name: "Amplicon", foreign_key: :reverse_primer_id
def amplicons
self.forward_amplicons + self.reverse_amplicons
end
end
OPTION 1 returns an array so you CANNOT chain more conditions, i.e. you CANNOT call primer.amplicons.where(...)
OPTION 2
class Primer
def amplicons
Amplicon.where('forward_primer_id = ? OR reverse_primer_id = ?', self.id, self.id)
end
end
OPTION 2 returns a relation, so you can chain more conditions to it.
has_many :forward_amplicons, class_name: "Amplicon", foreign_key: "forward_primer_id"
has_many :reverse_amplicons, class_name: "Amplicon", foreign_key: "reverse_primer_id"

Eager load polymorphic

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.

Rails Active Record: modifying and saving the child of an object from the result of joins?

So, this:
p = Person
.joins('join organization o on o.id = organization_id')
.where('o.id' => 1)
.select('person.*')
.first!
p.name = 'hi!'
p.save!
works as expected, saving the person's name.
But, how would I do this:
p.organization.name = 'bye!'
p.save!
I can't figure out the right projection to get the organization fields to map (or if it's possible). I've tried '*' and 'organization.name as "person.organization.name"'.
In order for what you're doing to work, you have to set the autosave option to true in your belongs_to :organization association.
belongs_to :organization, autosave: true
or just call save on the organization
p.organization.name = 'Org Name'
p.organization.save
You have to declare association in your Person class, using
belongs_to
has_one
has_many
has_many :through
has_one :through
or has_and_belongs_to_many,
and Rails will do the join by it self and link your both class together.
Let me paste here a section of Rails guide:
With Active Record associations, we can [..] tell Rails that there is a connection between the two models. Here’s the revised code for setting up customers and orders:
class Customer < ActiveRecord::Base
has_many :orders, :dependent => :destroy
end
class Order < ActiveRecord::Base
belongs_to :customer
end
With this change, creating a new order for a particular customer is easier:
#order = #customer.orders.create(:order_date => Time.now)
I suggest you read the complete guide here: http://guides.rubyonrails.org/association_basics.html

Associations and (multiple) foreign keys in rails (3.2) : how to describe them in the model, and write up migrations

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.

Resources