Creating link table to connect one to one relationship of same model - ruby-on-rails

I want to make sure table join with joining table has strong one-to-one relationship. Following is example
Run generator
bundle exec rails g model Page name:string content:text
bundle exec rails g model PageAssociation left_page_id:integer right_page_id:integer
After running these generator, I have to change my page model following
class Page < ApplicationRecord
has_one :left_page_association, :foreign_key => :left_page_id,
:class_name => 'PageAssociation'
has_one :right_page_association, :foreign_key => :right_page_id,
:class_name => 'PageAssociation'
has_one :left_page, :through => :left_page_association,
:source => :right_page
has_one :right_page, :through => :right_page_association,
:source => :left_page
end
In my association table model page_association
class PageAssociation < ApplicationRecord
belongs_to :left_page, class_name: 'Page'
belongs_to :right_page, class_name: 'Page'
end
Now using rails c I can do following
page_one = Page.create(name: 'first page', content: 'first page content')
page_two = Page.create(name: 'second page', content: 'second page content')
PageAssociation.create(left_page: page_one, right_page: page_two)
page_one.left_page
page_two.right_page
All working fine and it is returning pages. but I can do also this
page_three = Page.create(name: 'third page', content: 'third page content')
PageAssociation.create(left_page: page_one, right_page: page_three)
Of course it still shows on relationship due to has_one but it is creating another relationship. So I want following
Only one association can be created and it should not allow another one
What is best way to do this as we have million of records
Is it one to use page.left_page? or is there any other optimised way to do same thing.
Should I add indexing using following migration lines. Does it effect performance.
add_foreign_key :page_associations, :pages, column: :left_page_id
add_foreign_key :page_associations, :pages, column: :right_page_id
I changed migration to make column value unique, so it now give error when I create another PageAssociate with same page.id but is it right way?
t.integer :left_page_id, null: false, index: { unique: true }
t.integer :right_page_id, null: false, index: { unique: true }
what problem I am solving
So I have books table where I have multiple books, but some books borrow pages from one another. So suppose I have book-A and book-B so I want to show relationship of book-a to book-b. If they are related then I will create another relationship of book-a page-10 linked to book-b page 20, so when I click on book-a to synchronise, it will bring all changes of book-b page 20 in my book-a.
So above is my first step to connected two books. I understand solving it using self join and keys is best but I can't do as we have huge number of records so that is not possible. So I have used above method to do it. But I did add unique constraints on db and validations in model to solve it. but I feel it is still not good way of doing it.
later one I will make following
main-book , main-book-page, secondary-book-page
This will allow me to bound later pages each other. The name book and pages are fictions, actually entities are different.

What you want here is really just a normal many to many table setup:
class Book < ApplicationRecord
has_many :book_pages
has_many :pages, through: :book_pages
end
class Page < ApplicationRecord
has_many :book_pages
has_many :books, through: :book_pages
end
class BookPage < ApplicationRecord
belongs_to :book
belongs_to :page
validates_uniqueness_of :book_id, scope: :page_id
end
Uniqueness in this case can be guarenteed by adding a unique index:
add_index :book_pages, [:book_id, :page_id]
Why?
M2M join table setups where you have two foreign keys that are assigned arbitrarily are not a very good design and don't work well with ActiveRecord since you can't define assocations with an OR clause.
Each association can only have a single foreign key. This means that you can't treat it as a homogenius collection and cannot eager load it.
That means you need to write crappy joins like this instead of being able to work with a proper assocation:
Book.joins(
"LEFT JOIN pages_assocations pa ON pa.left_page_id = books.id OR pa.left_page_id = books.id"
)
And you also have to write steaming piles when creating indirect assocations.
While the table setup with a single row per book/page combo may seem to require more rows on the onset its also much more flexible as you can map out the assocations between books by subqueries, lateral joins or grouping and counting the number of matches.
class Book < ApplicationRecord
has_many :book_pages
has_many :pages, through: :book_pages
def books_with_pages_in_common
Book.where(
id: BookPage.select(:book_id)
.where(page_id: pages)
)
end
end

Related

Rails: Address model being used twice, should it be separated into two tables?

I am making an ecommerce site, and I have Purchases which has_one :shipping_address and has_one :billing_address
In the past the way I've implemented this is to structure my models like so:
class Address < ActiveRecord::Base
belongs_to :billed_purchase, class_name: Purchase, foreign_key: "billed_purchase_id"
belongs_to :shipped_purchase, class_name: Purchase, foreign_key: "shipped_purchase_id"
belongs_to :state
end
class Purchase < ActiveRecord::Base
INCOMPLETE = 'Incomplete'
belongs_to :user
has_one :shipping_address, class: Address, foreign_key: "shipped_purchase_id"
has_one :billing_address, class: Address, foreign_key: "billed_purchase_id"
...
end
As you can see, I reuse the Address model and just mask it as something else by using different foreign keys.
This works completely find, but is there a cleaner way to do this? Should I be using concerns? I'm sure the behavior of these two models will always be 100% the same, so I'm not sure if splitting them up into two tables is the way to go. Thanks for your tips.
EDIT The original version of this was wrong. I have corrected it and added a note to the bottom.
You probably shouldn't split it into two models unless you have some other compelling reason to do so. One thing you might consider, though, is making the Address model polymorphic. Like this:
First: Remove the specific foreign keys from addresses and add polymorphic type and id columns in a migration:
remove_column :addresses, :shipping_purchase_id
remove_column :addresses, :billing_purchase_id
add_column :addresses, :addressable_type, :string
add_column :addresses, :addressable_id, :integer
add_column :addresses, :address_type, :string
add_index :addresses, [:addressable_type, :addressable_id]
add_index :addresses, :address_type
Second: Remove the associations from the Address model and add a polymorphic association instead:
class Address < ActiveRecord::Base
belongs_to :addressable, polymorphic: true
...
end
Third: Define associations to it from the Purchase model:
class Purchase < ActiveRecord::Base
has_one :billing_address, -> { where(address_type: "billing") }, as: :addressable, class_name: "Address"
has_one :shipping_address, -> { where(address_type: "shipping") }, as: :addressable, class_name: "Address"
end
Now you can work with them like this:
p = Purchase.new
p.build_billing_address(city: "Phoenix", state: "AZ")
p.build_shipping_address(city: "Indianapolis", state: "IN")
p.save!
...
p = Purchase.where(...)
p.billing_address
p.shipping_address
In your controllers and views this will work just like what you have now except that you access the Purchase for an Address by calling address.addressable instead of address.billed_purchase or address.shipped_purchase.
You can now add additional address joins to Purchase or to any other model just by defining the association with the :as option, so it is very flexible without model changes.
There are some disadvantages to polymorphic associations. Most importantly, you can't eager fetch from the Address side in the above setup:
Address.where(...).includes(:addressable) # <= This will fail with an error
But you can still do it from the Purchase side, which is almost certainly where you'd need it anyway.
You can read up on polymorphic associations here: Active Record Association Guide.
EDIT NOTE: In the original version of this, I neglected to add the address_type discriminator column. This is pernicious because it would seem like it is working, but you'd get the wrong address records back after the fact. When you use polymorphic associations, and you want to associate the model to another model in more than one way, you need a third "discriminator" column to keep track of which one is which. Sorry for the mixup!
In addtion to #gwcoffey 's answer.
Another option would be using Single Table Inhertinace which perhaps suits more for that case, because every address has a mostly similar format.

Saving a rails has_many association using multiple keys

I have the following models:
class Product < ActiveRecord::Base
has_many :product_recommendation_sets, :dependent => :destroy
has_many :recommendation_sets, :through => :product_recommendation_sets
end
class RecommendationSet < ActiveRecord::Base
has_many :product_recommendation_sets, :dependent => :destroy
has_many :products, :through => :product_recommendation_sets
has_many :recommendations
end
class Recommendation < ActiveRecord::Base
belongs_to :recommendation_set
end
And am adding recommendations recommendations_set like so:
p = Product.find_by_wmt_id(product) || Product.create( ItemData.get_product_data(product) )
recommendation = find_by_rec_id(rec_id) || create( ItemData.get_product_data(rec_id) )
rec_set = RecommendationSet.find_or_create_by_rating_set_id_and_model_version_and_product_id(rating_set.id, model_version, p.id)
sec_set.update_attributes(
:rating_set_id => rating_set.id,
:product_id => p.id,
:model_version => model_version,
:notes => note
)
sec_set.recommendations << recommendation
sec_set.save
prs = ProductRecommendationSet.find_or_create_by_recommendation_set_id_and_rating_set_id_and_product_id(rec_set .id, rating_set.id, p.id,)
prs.update_attributes(
:recommendation_set_id => rec_set.id,
:rating_set_id => rating_set.id,
:product_id => p.id
)
This works as expected, however my problem is that I have multiple recommendation_sets which belong to multiple products, and each of the recommendation_sets may have the same recommendation. By saving each recommendation to a recommendation_set as I am currently doing, if two recommendation_sets have the same recommendation, only one of the sets will add that recommendation. Is there anyway of saving each recommendation to multiple recommendation_sets using a secondary id, such as save by recommendation_id_and_product_id, or would I need to change this releationship to a has_many :through?
Based on your clarification, I think you basically have a many-to-many relationship between RecommendationSet and Recommendation. Presently, you have a one-to-many.
There are a couple of options:
Use the has_and_belongs_to_many method in both models to describe the relationship;
Manually create a "join" model and then give both RecommendationSet and Recommendation a has_many to this join model (with two corresponding belongs_to lines in the join model pointing to the other two models);
A has_many ... :through style, like you mentioned
Note that the first two options require you to have a join table.
If you require additional information on the join table/model, I tend to go with the 2nd option. Otherwise, either the first or third are perfectly valid.
Ryan Bates of RailsCasts made an episode about this here: http://railscasts.com/episodes/47-two-many-to-many
And some more information from the Rails documentation: http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#label-Many-to-many
In short, if you don't need extra info on the join, I think your idea of the has_many ... :through is perfectly fine.
Let me know whether that helps

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 - data view from has_many and Has_many_and_belongs_to_many

I'm trying to implement this project:
http://img7.imagebanana.com/img/cnb46ti2/relationships.png
I want to let view the skills of an employee on the employee's show page
An employee has a position, and every position has skills which an employee of this position needs to know
so if I understand right, positions and skills have an n:m relationship, and they need a join table for a has_many_and_belongs_to_many relationship. Because a position includes many skills and every skill belongs to many positions.
now my questions
the position_skill-table -> is it better to use a has_and_belongs_to_many relationship, so this table has no own id or is it better to use a has_many :through relationship? I guess it's better do use a has_and_belongs_to_many relationship, because this relationship table will not have any further information inside than just the two keys. Am I right?
if I take a has_and_belongs_to_many - relationship, is that the only thing I need to write into the models?
a) class Position < ActiveRecord :: Base (...) has_and_belongs_to_many :skills (...)
b) class Skill < ActiveRecord :: Base (...) has_and_belongs_to_many :positions (...)
c) into db\migrate def self.up create_table :positon_skill, :id => false do |t| (...)
and after that, the positions and skills are connected with each other? Is that right? Did I forget something?
if that's right, how can I let the skills view on employee's show page? An employee has 1 position, and this position has several skills... What for code do I need to write into the show.html.erb of employee? Something like <%= employee.position.skill %>? Do I also need to render something? Sorry, I'm very confused and I think I read too much information in web... Or is there any description in web which exactly describes what I need for?
thanks alot in advance and sorry for that redundant question.
If you're sure you aren't going to want to add any information later to the position_skills table, has_and_belongs_to_many will work fine. However, has_many :through is far more flexible if you change your mind later and isn't much harder to set up.
If you use has_and_belongs_to_many, you only need association declarations in the models and the database table with position_id:integer and skill_id:integer fields. Seems like you've got that already.
To be able to access employee.position.skills in your view, you need to eagerly load the employee's associations. Try something like the following:
class EmployeesController < ApplicationController
...
def show
#employee = Employee.find(params[:id], :include => { :position => :skills })
end
...
end
I think that should work if you stick with has_and_belongs_to_many, but if you go for has_many :through (which I recommend), you'll need to use :include => { :position => { :position_skills => :skills } }
This is what it looks like in your diagram. Consider the following:
class Employee < ActiveRecord :: Base
belongs_to :position
...
end
class Position < ActiveRecord :: Base
has_many :employees
has_many :position_skills
has_many :skills, :through => :position_skills
...
end
class PositionSkill < ActiveRecord :: Base
belongs_to :position
belongs_to :skill
...
end
class Skill < ActiveRecord :: Base
has_many :position_skills
has_many :positions, :through => :position_skills
...
end
The only problem with this is that an employee is tied to a single position. While this position has many skills through positions. I would change it to position belongs_to employee and employee has_many positions. This leaves it open to track employees that move from one position to the next. Let me know if you need further info on that.

Do I need to manually create a migration for a HABTM join table?

I'm struggling now to get HATBM working correctly. I have a beaten scanario: articles and tags. I presume, HABTM should be used here, since it is a many-to-many relationship.
I don't know however if I should manually create a join table (articles_tags in this case).
My code currently as follows:
class Article < ActiveRecord::Base
has_and_belongs_to_many :tags
end
class Tag < ActiveRecord::Base
has_and_belongs_to_many :articles
end
When I run the migrations, no 3rd table is created.
Also, I would like to add that my third table doesn't bear any domain logic, just blind assignment.
I'm using Rails 2.2.2
You should do this in a migration of one of the tables, or in a separate migration if those migrations have been ran:
create_table :articles_tags, :id => false do |t|
t.references :article, :tag
end
add_index :articles_tags, [:article_id, :tag_id]
This will create the table for you and the :id => false tells Rails not to add an id field to this table. There's an index also, which will speed up lookups for this join table.
You could also generate a model (ArticlesTag) for this and do:
# article.rb
has_many :articles_tags
has_many :tags, :through => :articles_tags
# tag.rb
has_many :articles_tags
has_many :articles, :through => :articles_tags
# article_tag.rb
belongs_to :tag
belongs_to :article
And then create the table in the migration generated from the script/generate model articles_tag call.
Note that this is covered in the API.
http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#method-i-has_and_belongs_to_many
You probably also want to add an index to the migration:
add_index "articles_tags", "article_id"
add_index "articles_tags", "tag_id"
However, if you want tagging functionality I'd recommend the acts_as_taggable_on rails plugin:
http://www.intridea.com/tag/acts_as_taggable_on
http://github.com/mbleigh/acts-as-taggable-on/
I've used it on a project and it was very easy to implement.
One of the issues with a join table for tagging is that it can easily get ugly creating a join table for each content type you wish to make taggable (ie. comments_tags, posts_tags, images_tags, etc). This plugin uses a taggings table which includes a discriminator to determine the content type without the need of a specific join table for each type.
In combination with this Qeuestion(1st answear) How to set up a typical users HABTM roles relationship and 1st answear from here, it has to be understood even by a monkey. I am new in RoR and it's got working like a charm

Resources