Mutiple has_one of the same class - ruby-on-rails

Got a really interesting situation with all that has_one and belongs_to relationships when Rails loads its dependant models in an inversed manner.
Let us have a model Couple with two related models of the same class, User:
class Couple < ActiveRecord::Base
has_one :male, class_name: "User"
has_one :female, class_name: "User"
end
class User < ActiveRecord::Base
belongs_to :couple
end
In this situation, when we create a Couple and assign it two instances of User, we will get into this:
# create all the stuff
couple = Couple.new
he = User.create name: 'Bob'
she = User.create name: 'Sara'
couple.male = he
couple.female = she
couple.save
# here's where the gap begins:
couple.male.name # => 'Bob'
couple.female.name # => 'Sara'
# Ok, so far so good...
Couple.find(couple.id).male.name # => 'Bob'
# What's the ..?!
Couple.find(couple.id).female.name # => 'Bob'
And what I've seen in the console performing all these, is this:
> couple.female.name
'Sara'
# nothing happens as the model is loaded already
> Couple.find(couple.id).female.name
SELECT `couples`.* FROM `couples` WHERE `couples`.`id` = 2 LIMIT 1
SELECT `users`.* FROM `users` WHERE `users`.`couple_id` = 2 LIMIT 1
'Bob'
# sure, here's the trouble!
Hmmm... That's not good... Searching over the Internet guided me to this: I created two classes, MaleUser and FemaleUser, both derived from User model. And changed the belongs_to :couple to a belongs_to :couple, foreign_key: :his_id and ... :her_id. Yet, the same result I seen on the screen.
My question is, why the hell this happens and how to perform that loading in a correct manner? So that Couple.find(couple_id).she would gave me the proper object?
UPD: tables structure:
create_table :users do |t|
t.integer :couple_id
# ...
end
create_table :couples do |t|
t.integer :his_id
t.integer :her_id
# ...
end
Thanks!

The relationship for the users in Couple needs to be a belongs_to relationship and not has_one. E.g:
class Couple < ActiveRecord::Base
# ...
belongs_to :male, :class_name => 'User', :foreign_key => 'his_id'
belongs_to :female, :class_name => 'User', :foreign_key => 'her_id'
end
This tells ActiveRecord that a Couple has two User object relations. One named male that can be retrieved with the ID found in the his_id column of the Couple table and one named female who's ID is found in the her_id column.
Whereas has_one would look for this relationship data on the Users table (which doesn't exist). The users table only references the couple_id and not whether the user is the male or female user for the Couple relationship.

Related

Rails many to many polymorphic on both sides

Rails 5
Looking on how to do a join table with has many through relationships where both sides are polymorphic and the relationships are just parents or children which could be of any class
I'm building a Laboratory Information Management System and inside this system there are approximately 30 "data modules" (each their own model) which fall into three different categories and can generally be parents or children to any of the other 30 data module classes
So lets say I have DataModuleA - DataModuleZ (ie 26 modules one for each letter)
I want any of those modules to be either a parent or a child to other modules and I want to be able to call .child_modules or .parent_modules on any of those objects and get a list of all the parents or children
Right now I have a join table module_relationships
create_table :module_relationships, id: :uuid do |t|
t.references :child_module, polymorphic: true, type: :uuid, index: {:name => 'index_module_relationships_on_child_type_and_id'}
t.references :parent_module, polymorphic: true, type: :uuid, index: {:name => 'index_module_relationships_on_parent_type_and_id'}
t.timestamps
end
And then an abstract base class which all of the modules can inherit from
class ApplicationModule < ApplicationRecord
## maybe should go back to being a concern where individual has many throughs are determined at the class level
self.abstract_class = true
has_many :child_module_relationships, class_name: "ModuleRelationship", foreign_key: "parent_module"
has_many :parent_module_relationships, class_name: "ModuleRelationship", foreign_key: "child_module"
# has_many :child_modules, through: :child_module_relationships ## doesnt work now
# has_many :parent_modules, through: :parent_module_relationships ## doesnt work now
def create_child_module_relationship(child_module)
ModuleRelationship.create(
parent_module_id: self.id,
parent_module_type: self.class.to_s,
child_module_id: child_module.id,
child_module_type: child_module.class.to_s
)
end
def create_parent_module_relationship(parent_module)
ModuleRelationship.create(
child_module_id: self.id,
child_module_type: self.class.to_s,
parent_module_id: parent_module.id,
parent_module_type: parent_module.class.to_s
)
end
end
Right now trying to call .child_modules throws an error:
ActiveRecord::HasManyThroughAssociationPolymorphicSourceError (Cannot have a has_many :through association 'ApplicationModule#child_modules' on the polymorphic object 'ChildModule#child_module' without 'source_type'. Try adding 'source_type: "ChildModule"' to 'has_many :through' definition.)
Which more or less makes sense to me but I thought that was the whole point of defining the type on the join table so it would know where to look for the objects.
Should I just write a scope and do a nasty n+1 query for each ModuleRelationship Record?
Any other thoughts on how I could get the .child_modules and .parent_modules to return an a multi_model list of modules ?
Not sure if I understand your question correctly, but here is something that maybe helps you to generate some ideas for your solution.
An example where I'm storing countries and link countries to companies:
class Admin::SystemValues::Country < ApplicationRecord
has_many :linked_countries, class_name: "Common::LinkedCountry"
has_many :companies, class_name: "Common::Company", through: :linked_countries,
source: :countryable, source_type: "Common::Company"
end
class Common::LinkedCountry < ApplicationRecord
# == Schema Information
#
# Table name: common_linked_countries
#
# id :bigint(8) not null, primary key
# country_id :integer not null
# countryable_id :bigint(8) not null
# countryable_type :string not null
# updated_at :datetime
#
# Indexes
#
# index_common_linked_countries_on_country_and_countryable (country_id,countryable_id,countryable_type) UNIQUE
#
belongs_to :countryable, polymorphic: true
belongs_to :country, class_name: 'Admin::SystemValues::Country', optional: true
end
class Common::Company < ApplicationRecord
has_many :linked_countries, class_name: 'Common::LinkedCountry', as: :countryable
has_many :countries, class_name: 'Admin::SystemValues::Country', through: :linked_countries
end
So basically I can link Country to any model I want through country_id. I can get my countries with Common::Company.first.countries.
Maybe you can somehow store those DataModuleA - DataModuleZ (ie 26 modules one for each letter) within one model and then do something like above. For example maybe you can store an attribute like letter (a..z) and then you have all data in one model and table to query later.

How to a order a list of records based on how many of their associated records match records in a given array?

Sorry if the title's unclear, I didn't know how better to phrase it.
I have a model "Playlist" which has_many and belongs_to another model "User", through an intermediary model "PlaylistUser".
Let's say I'm on the page of a given Playlist (#playlist), and #users = #playlist.users. How can I list all the other Playlists, ordered by how many Users they share with #playlist?
So if #playlist.users = ["joe","nick","bob"], <playlist2>.users = ["nick","bob","tom"] and <playlist 3>.users = ["bob","jim","rich"], playlist2 should be listed first, because it shares 2 users with #playlist, while playlist3 only shares 1 user.
I hope I made what I'm trying to do clear enough, but let me know if additional clarification is needed.
Assocations:
class Playlist < ActiveRecord::Base
has_many :playlist_users
has_many :users, :through => :playlist_users
end
class PlaylistUser < ActiveRecord::Base
belongs_to :playlist
belongs_to :user
end
class User < ActiveRecord::Base
has_many :playlist_users
has_many :playlists, :through => :playlist_users
end
Playlist.joins(:users)
.where.not(id: #playlist.id)
.where(users: {id: #playlist.user_ids})
.group(:id)
.order('count(*) desc')
Or, if you need to access the result of count(*):
#playlists = Playlist.select('playlists.*, count(*) as shared_users_count')
.joins(...)
...
.order('shared_users_count desc')
# print out shared users count
#playlists.each { |pl| puts pl.shared_users_count }
This will simplify your models a little. You have already identified the "has and belongs to many" relationship, this is how to implement it.
First you need only two models. Playlist and User
class Playlist < ActiveRecord::Base
has_and_belongs_to_many :users
end
class User < ActiveRecord::Base
has_and_belongs_to_many :playlists
end
Next you need 3 tables.
class SampleMigration < ActiveRecord::Migration
def change
create_table :users do |t|
t.string :name
end
create_table :playlists do |t|
t.string :name
end
create_table :playlists_users, id: false do |t|
t.belongs_to :playlist
t.belongs_to :user
end
end
end
Note that plurals are important in naming collections, for example has_and_belongs_to_many :users and as names for your table create_table :users.
The join table does not need a model, but does need to be named in alphabetical order and the names pluralised.
Now you can use user.playlists to return an array of playlists connected to this user. Being an array you can call user.playlists[5] to get the 5th song in the list or you can iterate over it user.playlists.each do |list|.
So if #playlist.users = ["joe","nick","bob"], .users = ["nick","bob","tom"] and .users = ["bob","jim","rich"], playlist2 should be listed first, because it shares 2 users with #playlist, while playlist3 only shares 1 user.
There are two ways you can achieve this. Through a helper method where you retrieve all lists and union them in Rails, or through an SQL query. SQL is faster, but requires knowledge of SQL. Initially I would use Rails and tackle the SQl when there is a gain to be made.
One implementation might be (NOTE: treat this as pseudo code, I have not tested it);
# get all the lists
newPlaylist = array.new
users = #playlist.users
users.each do |user|
newPlaylist = newPlaylist + user.playlists
end
# count all the lists
countPlaylists = Hash.new
newPlaylist.each do |list|
if list.in? countPlaylists.keys
countPlaylists[list] = countPlaylists[list] + 1
else
countPlaylists[list] = 1
end
end
# sort the list - I'm not sure if it sorts on keys or values,
# but either way you should be able to figure it out
countPlaylists.sort

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.

Rails updating a many to many record

I'm getting stuck at trying to update an existing many to many record.
Project model:
class Project < ActiveRecord::Base
belongs_to :assignment
belongs_to :programmer
end
Programmer model:
class Programmer < ActiveRecord::Base
has_many :projects
has_many :assignments, :through => :projects
end
Assignment model:
class Assignment < ActiveRecord::Base
has_many :projects
has_many :programmers, :through => :projects
end
so I have data linked up like so:
p = Programmer.create(:name => "Mike")
p.assignments.create(:name => "homework4")
p.assignments[0] = Assignment.find_or_create_by_name("homework1")
p.save
so as you can see, I'm trying to update the association of Mike's first hw to "homework1". All the homework assignments are already in the assignments table so it shoud just find "homework1" and assign it to mike. unfortunately, when I type the third line there are no errors, but it doesn't update it. In memory, p.assignments == homework1, but in the DB it's still the same(even after p.save). The project's join table isn't changed at all.
the logs of mysql show this command being generated whenever I enter the 3rd line.
SELECT "assignments".* FROM "assignments" WHERE "assignments"."name" = 'homework1' LIMIT 1
there's no Update anywhere.... what am I doing wrong?
UPDATE
So I found out that I could just reference the join table directly to edit the links. Something along the lines of:
proj = p.projects.first
proj.assignment_id = 12
proj.save!
If you just want a reference to the object, then you need to edit your migration scripts (db/migrate). An example:
def self.up
create_table :configurations do |t|
t.string :name
t.references :project # This store just the id of the object.
t.timestamps
end
end
Don't forget to type:
rake db:migrate

has_many and single table inheritance

I have a has_many relationship between two entities, Feeds and Posts. I also have specific types of posts, Videos and Photos. This is structured in the database using single table inheritance.
Right now I have my Feed model specifying a has_many relationship between Feeds and Posts (including the subtypes)
class Feed < ActiveRecord::Base
has_many :posts
has_many :photos
has_many :videos
Is there a better, more conventional way to specify this? Or is what I have as simple as it can get?
If i understand you correctly you have Posts and posts can be either video or photo. as Jaryl said what you have is probably the easiest to understand/handle however if you wanted to get fancy you could use single table inheritance or polymophic associations.
STI - example (from Agile Web Development with Rails 3rd Edition)
create_table :people, :force => true do |t|
t.string :type
#common attributes
t.string :name
t.string :email
#attributes for type=Customer
t.decimal :balance, :precision => 10, :scale => 2
#attributes for type=Employee
t.integer :reports_to
t.integer :dept
#attributes for type=Manager
#none
end
class Person < ActiveRecord::Base
end
class Customer < Person
end
class Employee < Person
belongs_to :boss, :class_name => "Manager", :foreign_key => :reports_to
end
class Manager < Person
end
So if you create a customer
Customer.create(:name => 'John Doe', :email => 'john#doe.com', :balance => 78.29)
you can then find it via person
x = Person.find_by_name('John Doe')
x.class #=> Customer
x.email #=> john#doe.com
x.balance #=> 78.29
x.some_customer_class_method # will work because the Person.find method returned a Customer object
So you could have
class Post < ActiveRecord::Base
end
class Photo < Post
end
class Video < Post
end
and then you could find them all by Post.all but you would get back Photo and Video objects (and post objects if you have posts that are not photo or video)
don't forget the string :type in your db table
This is pretty much the simplest you can do.
Well, if photos could be treated the same as videos, then perhaps you could do away with STI and use named scopes to provide accessors to different types of content.
I agree that the example in the question is as simple as it gets. It is already using STI and clearly states the associations.
Also, you could rip out the STI later, and split :photos and :videos into their own separate tables without changing the Feed model's code one bit. Score!

Resources