Rails 4 has_one self reference column - ruby-on-rails

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.

Related

Rails Composite Keys - Setting up models and assocs

I'm using Rails Composite Keys for the first time and even though I've read the docs, I'm still unsure how to set up the models correctly.
An Activity can be Scheduled by a Provider. I want a join table that tracks activity_id and provider_id as a unique composite key so that I can associate it with something else (prices). The composite needs to be generated upon the creation of a new schedule. The provider does not necessarily own the activity themselves directly.
I've got this for my composite key as it's own model. Is this right?
class ScheduledActivity < ActiveRecord::Base
self.primary_keys = :provider_id, :activity_id
belongs_to :activity
belongs_to :provider
has_many :schedules, :foreign_key => [:provider_id, :activity_id]
has_many :prices, :foreign_key => [:provider_id, :activity_id]
end
This for the db migration:
class CreateJoinTableScheduledActivities < ActiveRecord::Migration
def change
create_join_table :providers, :activities, table_name: :scheduled_activities do |t|
t.index [:provider_id, :activity_id]
end
end
end
How do I then get new entries on the join table as Schedules are created? Do I put only belongs_to :scheduled_activity on this model - i.e. no provider and activity ids on the Schedule table, and do I write a separate hook in order to create the new composite keys?
(Also - is this the correct use case for composite keys in the first place?!)
Thanks in advance!
Well I'm not sure if this fully answers your question but if you have a composite unique key you can do something like the following.
In your migration:
add_index :scheduled_activities, [:provider_id, :activity_id], unique: true
In your join model:
belongs_to :activity
belongs_to :provider
validates :activity, presence: true, uniqueness: { scope: :provider }
Given that this is more than a simple join table maybe you should consider using id's to identify each ScheduledActivity, then you can simply add that reference to your Schedule model/table (Schedule belongs_to :schedule_activity - schedules table will have a reference to schedule_activity with attribute schedule_activity_id, instead of using a composite key). It would make things much simpler and efficient.

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.

has_one with more than one possible foreign key column

I have a Family class that includes a mother_id and a father_id. From the Family model's perspective, it's important to know which parent is the mother and which is the father, but the mother and father as Residents have all the same attributes (i.e. database columns). So ideally, I'd like my model files to look like this:
class Resident < ActiveRecord::Base
has_one :family, :dependent => :nullify, :foreign_key => :father_id
has_one :family, :dependent => :nullify, :foreign_key => :mother_id
attr_accessible :email, :cell, :first_name, :last_name
end
class Family < ActiveRecord::Base
belongs_to :father, :class_name => 'Resident', :foreign_key => 'father_id'
belongs_to :mother, :class_name => 'Resident', :foreign_key => 'mother_id'
attr_accessible :address, :city, :state, :number_of_children
end
This doesn't work. my_family.mother and my_family.father work, so Rails seems to be happy with the double belongs_to. However, my_dad.family == nil, indicating that the second has_one is overriding the first. This is reasonable, because otherwise, what would happen if a resident_id showed up in both the mother_id and father_id columns? (While I plan to add model-level validation to ensure that never happens, has_one doesn't talk to validation methods.) Furthermore, what would my_dad.family = Family.new mean? How would ActiveRecord choose whether to insert my_dad.id into Family.mother_id or Family.father_id?
From this Stackoverflow question, I got the idea to use different names, i.e. change the has_one lines to:
has_one :wife_and_kids, :class_name => 'Family', :dependent => :nullify, :foreign_key => :father_id
has_one :husband_and_kids, :class_name => 'Family', :dependent => :nullify, :foreign_key => :mother_id
My questions are:
1) Is there a better way to do it? A different DB schema, perhaps?
2) Is database-level validation possible to supplement the model-level validation to ensure that my_dad.id can't show up in both the mother_id and father_id columns?
3) Can you think of better names than husband_and_kids / wife_and_kids? (Admittedly not a programming question...)
EDIT:
It occurred to me to add a family getter:
def family
#family ||= self.wife_and_kids || self.husband_and_kids
end
after_save :reset_family
def reset_family
#family = nil
end
This makes it syntactically cleaner (since I really wasn't a fan of [husband|wife]_and_kids), without creating any ambiguity since there's no setter.
The main issue you're facing is that you have a "conditional" foreign key, meaning the foreign key used to resolve the :family of a resident depends on whether the resident is a male or female (mother or father). The best way to deal with this in my opinion is to use STI (Single-Table Inheritance) to differentiate between the two cases.
class Resident < ActiveRecord::Base
attr_accessible :email, :cell, :first_name, :last_name
end
class Mother < Resident
has_one :family, :dependent => :nullify, :foreign_key => :mother_id
end
class Father < Resident
has_one :family, :dependent => :nullify, :foreign_key => :father_id
end
You can still use the Resident table, but you'll need to migrate a :type field of type string and store the value "Mother" or "Father" depending on the case. Also, place each of these class definitions in its own file in models/.
Edit: I think this also resolves the issues suggested in your second and third questions.
Edit2:
Given the current schema, you would need to create a check constraint on your families table. For one, active record doesn't have direct support for this, so you would have to execute raw sql to add the constraint. In theory, each time a value is added or changed in the "mother_id" column of "families", the check would have to cross reference with the "residents" table, ascertaining that the "type" column of the "resident" is "Mother." The SQL that would (theoretically) add this constraint is
ALTER TABLE families
ADD CONSTRAINT must_be_mother CHECK ((SELECT type FROM residents WHERE residents.id = families.mother_id) = 'Mother')
The problem is that this CHECK contains a subquery, and as far as I know, subqueries in checks are disallowed by many databases. (See this question for specifics).
If you really want to implement a database-level validation here, you will likely need to change the schema by separating "residents" into "mothers" and "fathers."

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.

HABTM - uniqueness constraint

I have two models with a HABTM relationship - User and Role.
user - has_and_belongs_to_many :roles
role - belongs_to :user
I want to add a uniqueness constraint in the join (users_roles table) that says the user_id and role_id must be unique. In Rails, would look like:
validates_uniqueness_of :user, :scope => [:role]
Of course, in Rails, we don't usually have a model to represent the join relationship in a HABTM association.
So my question is where is the best place to add the constraint?
You can add uniqueness to join table
add_index :users_roles, [ :user_id, :role_id ], :unique => true, :name => 'by_user_and_role'
see In a join table, what's the best workaround for Rails' absence of a composite key?
Your database will raise an exception then, which you have to handle.
I don't know any ready to use rails validation for this case, but you can add your own validation like this:
class User < ActiveRecord::Base
has_and_belongs_to_many :roles, :before_add => :validates_role
I would just silently drop the database call and report success.
def validates_role(role)
raise ActiveRecord::Rollback if self.roles.include? role
end
ActiveRecord::Rollback is internally captured but not reraised.
Edit
Don't use the part where I'm adding custom validation. It kinda works but there is better alternatives.
Use :uniq option on association as #Spyros suggested in another answer:
class Parts < ActiveRecord::Base
has_and_belongs_to_many :assemblies, :uniq => true, :read_only => true
end
(this code snippet is from Rails Guides v.3). Read up on Rails Guides v 3.2.13 look for 4.4.2.19 :uniq
Rails Guide v.4 specifically warns against using include? for checking for uniqueness because of possible race conditions.
The part about adding an index to join table stays.
In Rails 5 you'll want to use distinct instead of uniq
Also, try this for ensuring uniqueness
has_and_belongs_to_many :foos, -> { distinct } do
def << (value)
super value rescue ActiveRecord::RecordNotUnique
end
end
I think that using :uniq => true would ensure that you get no duplicate objects. But, if you want to check on whether a duplicate exists before writing a second one to your db, i would probably use find_or_create_by_name_and_description(...).
(Of course name and description are your column values)
I prefer
class User < ActiveRecord::Base
has_and_belongs_to_many :roles, -> { uniq }
end
other options reference here

Resources