Polymorphic has_one association and multiple inheritance with Rails 3 - ruby-on-rails-3

I've seen some posts dealing with this, and am trying to determine the best solution.
Semantically, I want a Client model with a one-to-one relationship with a Survey. There are different kinds of surveys that have different fields but I want to share a significant amount of code between them. Because of the different fields I want different database tables for the surveys. There is no need to search across different types of surveys. It feels like I want the foreign key in the Client table for fast retrieval and potential eager-loading of the Survey.
So theoretically I think I want polymorphic has_one and multiple inheritance something like this:
class Client < ActiveRecord::Base
has_one :survey, :polymorphic => true
end
class Survey
# base class of shared code, does not correspond to a db table
def utility_method
end
end
class Type1Survey < ActiveRecord::Base, Survey
belongs_to :client, :as => :survey
end
class Type2Survey < ActiveRecord::Base, Survey
belongs_to :client, :as => :survey
end
# create new entry in type1_surveys table, set survey_id in client table
#client.survey = Type1Survey.create()
#client.survey.nil? # did client fill out a survey?
#client.survey.utility_method # access method of base class Survey
#client.survey.type1field # access a field unique to Type1Survey
#client2.survey = Type2Survey.create()
#client2.survey.type2field # access a field unique to Type2Survey
#client2.survey.utility_method
Now, I know Ruby does not support multiple inheritance, nor does :has_one support :polymorphic. So is there a clean Ruby way to achieve what I'm getting at? I feel like it's right there almost...

Here's how I would do this:
class Client < ActiveRecord::Base
belongs_to :survey, :polymorphic => true
end
module Survey
def self.included(base)
base.has_one :client, :as => :survey
end
def utility_method
self.do_some_stuff
end
end
Type1Survey < ActiveRecord::Base
include Survey
def method_only_applicable_to_this_type
# do stuff
end
end
Type2Survey < ActiveRecord::Base
include Survey
end

Related

How to reduce queries on associated object with Rails ActiveRecord

I have the following problem.
I have an Organization class that returns and array of "authorized user" emails and their associated User ID.
class Organization < ApplicationRecord
...
has_many :authorized_users
def authorized_user_opts
self.authorized_users.map do |authorized_user|
[authorized_user.email, authorized_user.user.id]
end
end
end
Then the AuthorizedUser class - note that we lookup the user via a find_by:
class AuthorizedUser < ApplicationRecord
...
def user
User.find_by(email: email)
end
end
And the User model:
class User < ApplicationRecord
validates :email, presence: true
end
This creates an extra query for each user to get their ID. Is there a way I can improve this query?
I thought about migrating the AuthorizedUser class to add a user_id field, but I'm wondering if there's a way to improve this just SQL instead of adding another field.
I think the addition of a user_id to AuthorizedUser is a decent choice, but if you must do it without, you should be able to use joins and includes:
authorized_users.joins("inner join users on users.email = authorized_users.email").includes(:users)
joins here is doing what an ActiveRecord association would do under the hood, and then includes eager loads the user objects in one query so that you don't have N queries for N users.
You might also be able to mess with the options on belongs_to which lets you specify the key that it uses under the hood. Something like:
# authorized_user.rb
belongs_to :user, foreign_key: 'email', primary_key: 'email'

Avoiding duplicate rows with many-to-many rails associations

I've seen a lot of many-to-many associations, but it seems that the common trend is that they end up using has_many :through relationships.
Let's say you have the following:
class User < ActiveRecord::Base
has_many :user_relationships
has_many :relations, :through => :user_relationships
end
class UserRelationship < ActiveRecord::Base
belongs_to :user
belongs_to :related_user, class_name: "User"
end
Given this type of relationship you end up setting the relationship up as follows:
user1.relations << user2
user2.relations << user1
Or this:
user1.user_relationships.build(related_user_id: 2)
user2.user_relationships.build(related_user_id: 1)
Resulting in rows in the join table looking like this:
user_id | related_user_id
1 | 2
2 | 1
So that when setting up the relation such as the above, you can see that the following can be accomplished
user1.relations.include? user2 = true
user2.relations.include? user1 = true
My question is: Is there a way to accomplish the above, or at least something similar to the above in speed, in Rails WITHOUT having to create 2 rows for every single two-way relationship and maintain the ability to see the relationship from both ends in an efficient manner, reducing the space complexity of creating this relationship by half...
Apologies if this is a noobie question, but I'm new to Rails, just starting to get the hang of things. It's easy to find out how to set these up, but I find it much harder to find out how to actually implement them in an efficient manner
Here's (roughly) what I've done. I'm going to skip a ton of the detail, but am happy to share more if it's helpful. This may be heinous, so I'm curious about others' thoughts.
Essentially, I have a Relationship model something like:
module ActsAsRelatingTo
class Relationship < ActiveRecord::Base
validates :owner_id, presence: true
validates :owner_type, presence: true
validates :in_relation_to_id, presence: true
validates :in_relation_to_type, presence: true
belongs_to :owner, polymorphic: true
belongs_to :in_relation_to, polymorphic: true
acts_as_taggable
acts_as_taggable_on :roles
end
end
Then, I've created a acts_as_relating_to module (again, glossing over the details) that includes stuff like:
module ActsAsRelatingTo
def acts_as_relating_to(*classes_array)
# re-opens the class at run time so I can do things like add
# new instance methods on-the-fly
class_eval do
before_destroy :tell_to_unrelate
has_many :owned_relationships,
as: :owner,
class_name: "ActsAsRelatingTo::Relationship",
dependent: :destroy
has_many :referencing_relationships,
as: :in_relation_to,
class_name: "ActsAsRelatingTo::Relationship",
dependent: :destroy
# iterates through arguments passed in 'acts_as_relating_to' call
# in the Person model, below.
classes_array.each do |class_sym|
# This is a method created on-the-fly. So, when I call
# (in the Person model, below) 'acts_as_relating_to :people',
# then I get a method on each instance of 'Person' called
# 'people_that_relate_to_me. You can also create class methods
# using `define_singleton_method`.
#
# The reason for doing all of this via the define_method
# is that it lets me create any number of 'things_i_relate_to'
# methods on any class descending from ActiveRecord::Base
# (more on this in a bit). Which, if I understand it
# correctly, is how a lot of the ActiveRecord functionality gets
# into a model in the first place.
define_method(class_sym.to_s + "_that_relate_to_me") do |options={}|
... some stuff
end
# Same as above, but know I'm defining a method called
# 'people_i_relate_to'
define_method(class_sym.to_s+"_i_relate_to") do |options={}|
... some more stuff
end
# I can also create static methods and incorporate them in the
# 'Person' class. I just define them in modules (such as
# ActsAsRelatingTo::InstanceMethods and ActsAsRelatingTo::ClassMethods)
# and then either 'include' (for instance methods) or 'extend'
# (for class methods) them.
include InstanceMethods
extend ClassMethods
end
end
end
end
# Here, I'm telling ActiveRecord::Base to 'extend' this module.
# That makes the 'acts_as_relating_to' method available in
# any class that descends from ActiveRecord::Base.
ActiveRecord::Base.extend ActsAsRelatingTo
Then, I can do something like:
class Person < ActiveRecord::Base
# Here, I am calling the method that I defined above, passing in
# :people, :organizations, and :programs. This is exactly the
# sort of thing you do all the time when you say something like
# 'has_one :foo', or 'belongs_to :bar'.
acts_as_relating_to :people, :organizations, :programs
# Here, I am calling a method I have that builds on acts_as_relating_to,
# but which I did not show, that creates administrative methods on
# the person so that I can say stuff like 'person.administrate organization'.
# Or, 'organization.administrators'.
acts_as_administering :organizations, :programs
...
end
So, if I have person_1 and person_2 and I have one relationship record where the owner is person_1, and the in_relation_to is person_2, then I can say person_1.people_i_relate_to and get back person_2. Or, I can say, person_2.people_that_relate_to_me and get back person_1.
I've also go an acts_as_administering module which builds on the acts_as_relating_to module and lets me do stuff like person_1.administered_organizations and person_1.administered_programs.
I've probably rattled on too long. Anyway, if it's interesting, I can say more.
Cheers!

How can I query a has_many through relationship which contains an additional attribute in the join table?

Situation
In my application a user can create a plan. Once the plan is created, the user can define the stakeholders/team members of the plan. Each team member becomes a responsibility assigned. There are many plans and users can be stakeholders of multiple plans and in each plan they have a different responsibility.
Example
Admin creates a plan and assigns 10 users as stakeholders. 1 is accountable, 2 are responsible, 7 just need to be informed
What I did so far
I set up a has_many through relationship between two models:
class User < ActiveRecord::Base
has_many :assignments
has_many :plans, through: :assignments
end
class Plan < ActiveRecord::Base
has_many :assignments
has_many :users, through: :assignments
end
The assignment table looks like this:
create_table :assignments do |t|
t.belongs_to :user
t.belongs_to :plan
t.string :responsibility
end
add_index :assignments, [:user_id, :plan_id]
the column responsibility contains one of 4 different values (responsible, accountable, informed, consulted.)
What I am looking for
I know how I can query all users that have been assigned to the plan (#plan.users.to_a) but I do not know how I can additionally supplement the user information with the responsibility they have in this plan.
The query I need is something along the lines of:
Select users which belong to plan X by looking at the assignment table. Do not just use the assignment table to identify the user, but also take the value from the responsibility column in the assignment table and return an array which contains:
user.first_name
user.last_name
user.responsibility (for this specific plan)
We had this exact requirement, and solved it in 2 ways:
Use an SQL Alias Column
The first way is to use an SQL Alias Column & append it to your has_many association, like this:
Class User < ActiveRecord::Base
has_many :assignments
has_many :plans, -> { select("#{User.table_name}.*, #{Plan.table_name}.responsibility AS responsibility") }, through: :assignments, dependent: :destroy
end
This will allow you to call #user.plans.first.responsibility, and will fail gracefully if no record exists
Use ActiveRecord Association Extensions
This is the best, but more complicated, way, as it uses the proxy_association object in memory (instead of performing another DB request). This script took us 2 weeks to create, so we're very proud of it!! Not tested with Rails 4.1:
#app/models/user.rb
Class User < ActiveRecord::Base
has_many :assignments
has_many :plans, through: :assignments, extend: Responsibility
end
#app/models/plan.rb
Class Plan < ActiveRecord::Base
attr_accessor :responsibility
end
#app/models/concerns/Responsibility.rb
module Responsibility
#Load
def load
captions.each do |caption|
proxy_association.target << responsibility
end
end
#Private
private
#Captions
def captions
return_array = []
through_collection.each_with_index do |through,i|
associate = through.send(reflection_name)
associate.assign_attributes({responsibility: items[i]}) if items[i].present?
return_array.concat Array.new(1).fill( associate )
end
return_array
end
#######################
# Variables #
#######################
#Association
def reflection_name
proxy_association.source_reflection.name
end
#Foreign Key
def through_source_key
proxy_association.reflection.source_reflection.foreign_key
end
#Primary Key
def through_primary_key
proxy_association.reflection.through_reflection.active_record_primary_key
end
#Through Name
def through_name
proxy_association.reflection.through_reflection.name
end
#Through
def through_collection
proxy_association.owner.send through_name
end
#Responsibilities
def items
through_collection.map(&:responsibility)
end
#Target
def target_collection
#load_target
proxy_association.target
end
end
Query the appointement table directly fitering for all the users in the current plan:
Appointement.select(:id).where(user_id: Plan.find(params[:id]).users.pluck(:user_id), plan_id: params[:id]).group(:id).having('count(*) = ?', Plan.find(params[:id]).users.count)

Returning associations for specific model when there is a polymorphic association in Rails 3.2

I have a polymorphic association in a Rails 3 app where a User may favorite objects of various classes.
class Favorite < ActiveRecord::Base
belongs_to :user
belongs_to :favoriteable, :polymorphic => true
end
class User < ActiveRecord::Base
has_many :favorites
end
class Image < ActiveRecord::Base
has_many :favorites, :as => :favoriteable
end
class Video < ActiveRecord::Base
has_many :favorites, :as => :favoriteable
end
I would like to be able return a list of just a User's favorite_images for example.
user.favorite_images #returns a list of the images associated with the user via :favoritable
I'm guessing there is a straightforward way of doing this but I haven't been able to figure it out. Let me know if you need anymore info.
Thanks!
===edit====
I know that I could retrieve what I am looking for via
favorite_images = user.favorites.collect{|f| if f.favoriteable_type=='Image'; f.favoriteable; end}
I could define an instance method for the User class and put that inside. I was wondering if there is a way to do it as some sort of has_many association. Really just because going forward it would be easier to have all that in one place.
When you created the table for Favorite you created a couple of columns favoriteable_id and favoriteable_type and you can use this information to restrict your query.
If you do user.favorites you will get all of the favorites and to restrict them to say just the images then you can do user.favorites.where(favoriteable_type: 'image') but that just gives you the favorite records and it sounds like you want the actual images. To get those you can do it by then mapping and pulling the favoriteable out. You'll likely want to include it in the query though so you don't hit the database so much. I would also make this a method on User.
def favorite_images
favorites.includes(:favoriteable).where(favoriteable_type: 'Image').map(&:favoriteable)
end

How to "merge" more than one 'ActiveRecord::Associations' and one or more scope methods?

I am using Ruby on Rails 3.2.2 and MySQL. I would like to "merge" the result of more than one ActiveRecord::Associations and one or more scope methods. That is, I have:
class User < ActiveRecord::Base
has_many :a_user_relationships, :foreign_key => 'a_key'
has_many :b_user_relationships, :foreign_key => 'b_key'
has_many :a_articles, :through => :a_user_article_associations # Returns objects kind of 'Article'
has_many :b_articles, :through => :b_user_article_associations # Returns objects kind of 'Article'
end
class Article < ActiveRecord::Base
# Note: This is a scope method.
def self.public
where(:status => 'public')
end
end
Given the above code I would like to run some method (as-like the following) so to retieve all public "a" and "b" user articles by executing as few as possible database queries:
#user.all_articles
class User < ActiveRecord::Base
# Note: Code in the following method is incorrect, but maybe it helps
# understanding what I mean.
def all_articles
self.(a_articles & b_articles).public
(self.a_articles.public & self.b_articles.public)
end
end
Is it possible?
Since you have two separate associations, you'll need at least two queries.
If you always want articles from both here, is there anything stopping you from having a simple "articles" association in addition to or instead of these two separate ones? That way, you could retrieve your articles with self.articles.public.