Rails Composite Keys - Setting up models and assocs - ruby-on-rails

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.

Related

Rails 4 has_one self reference column

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.

Change reference to different model rails migration

How can I migrate the Referral table to point to point to the PatientProfile id instead of the User id for an existing database (i.e. would like to migrate all existing rows)?
class User
belongs_to :profile, polymorphic: true
has_many :referrals
class PatientProfile
has_one :user, as: :profile
class Referral
belongs_to :user
I could accomplish this by simply making a migration and renaming the user_id column to be patient_profile_id and changing the reference in the model, however that would not migrate over existing records.
I wouldn't simply change the column name. I'd do something like this
class AddPatientProfileToReferrals < ActiveRecord::Migration
def up
add_column :referrals, :patient_profile_id
add_index :referrals, :patient_profile_id
Referral.reset_column_information
Referral.includes(:user).find_each do |referral|
referral.update_attribute(:patient_profile_id, referral.user.profile_id)
end
remove_column :referrals, :user_id
end
end
You probably want to make sure the referrals have a user, and you can simply reverse the process to write the down method.

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.

Multiple foreign keys referencing the same table in RoR

I want a Customer to reference two Address models, one for the billing address and one for the shipping address. As I understand it, the foreign key is determined by its name, as _id. Obviously I can't name two rows address_id (to reference the Address table). How would I do this?
create_table :customers do |t|
t.integer :address_id
t.integer :address_id_1 # how do i make this reference addresses table?
# other attributes not shown
end
This can be kind of confusing to people new to Rails (as I was recently), because some parts of the answer take place in your Migrations and some in your Models. Also, you actually want to model two separate things:
An address belongs to a single customer and each customer has many addresses. In your case this would be either 1 or 2 addresses, but I would encourage you to consider the possibility that a customer can have more than one shipping address. As an example, I have 3 separate shipping addresses with Amazon.com.
Separately, we want to model the fact that each customer has a billing address and a shipping address, which might instead be the default shipping address if you allow more than one shipping address.
Here's how you would do that:
Migrations
class CreateCustomers < ActiveRecord::Migration
create_table :customers do |t|
def up
t.references :billing_address
t.references :shipping_address
end
end
end
Here you are specifying that there are two columns in this table that will be referred to as :billing_address and :shipping_address and which hold references to another table. Rails will actually create columns called 'billing_address_id' and 'shipping_address_id' for you. In our case they will each reference rows in the Addresses table, but we specify that in the models, not in the migrations.
class CreateAddresses < ActiveRecord::Migration
create_table :addresses do |t|
def up
t.references :customer
end
end
end
Here you are also creating a column that references another table, but you are omitting the "_id" at the end. Rails will take care of that for you because it sees that you have a table, 'customers', that matches the column name (it knows about plurality).
The reason we added "_id" to the Customers migration is because we don't have a "billing_addresses" or "shipping_addresses" table, so we need to manually specify the entire column name.
Models
class Customer < ActiveRecord::Base
belongs_to :billing_address, :class_name => 'Address'
belongs_to :shipping_address, :class_name => 'Address'
has_many :addresses
end
Here you are creating a property on the Customer model named :billing_address, then specifying that this property is related to the Address class. Rails, seeing the 'belongs_to', will look for a column in the customers table called 'billing_address_id', which we defined above, and use that column to store the foreign key. Then you're doing the exact same thing for the shipping address.
This will allow you to access your Billing Address and Shipping Address, both instances of the Address model, through an instance of the Customer model, like this:
#customer.billing_address # Returns an instance of the Address model
#customer.shipping_address.street1 # Returns a string, as you would expect
As a side note: the 'belongs_to' nomenclature is kind of confusing in this case, since the Addresses belong to the Customers, not the other way around. Ignore your intuition though; the 'belongs_to' is used on whichever thing contains the foreign key which, in our case, as you will see, is both models. Hah! how's that for confusing?
Finally, we are specifying that a Customer has many addresses. In this case, we don't need to specify the class name this property is related to because Rails is smart enough to see that we have a model with a matching name: 'Address', which we'll get to in a second. This allows us to get a list of all of Customer's addresses by doing the following:
#customer.addresses
This will return an array of instances of the Address model, regardless of whether they are billing or shipping addresses. Speaking of the Address model, here's what that looks like:
class Address < ActiveRecord::Base
belongs_to :customer
end
Here you're accomplishing the exact same thing as with the 'belongs_to' lines in the Customer model, except that Rails does some magic for you; looking at the property name ('customer'), it sees the 'belongs_to' and assumes that this property references the model with the same name ('Customer') and that there is a matching column on the addresses table ('customer_id').
This allows us to access the Customer that an Address belongs to like this:
#address.customer # Returns an instance of the Customer model
#address.customer.first_name # Returns a string, as you would expect
This sounds like a has_many relationship to me - put the customer_id in the Address table instead.
Customer
has_many :addresses
Address
belongs_to :customer
You can also provide a foreign key and class in the assoc declaration
Customer
has_one :address
has_one :other_address, foreign_key => "address_id_2", class_name => "Address"
I figured out how to do it thanks to Toby:
class Address < ActiveRecord::Base
has_many :customers
end
class Customer < ActiveRecord::Base
belongs_to :billing_address, :class_name => 'Address', :foreign_key => 'billing_address_id'
belongs_to :shipping_address, :class_name => 'Address', :foreign_key => 'shipping_address_id'
end
The customers table includes shipping_address_id and billing_address_id columns.
This is essentially a has_two relationship. I found this thread helpful as well.
In Rails 5.1 or greater you can do it like this:
Migration
create_table(:customers) do |t|
t.references :address, foreign_key: true
t.references :address1, foreign_key: { to_table: 'addresses' }
end
This will create the fields address_id, and address1_id and make the database level references to the addresses table
Models
class Customer < ActiveRecord::Base
belongs_to :address
belongs_to :address1, class_name: "Address"
end
class Address < ActiveRecord::Base
has_many :customers,
has_many :other_customers, class_name: "Customer", foreign_key: "address1_id"
end
FactoryBot
If you uses FactoryBot then your factory might look something like this:
FactoryBot.define do
factory :customer do
address
association :address1, factory: :address
end
end
I had the same problem and solved doing this:
create_table :customers do |t|
t.integer :address_id, :references => "address"
t.integer :address_id_1, :references => "address"
# other attributes not shown
end

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