Defining "has_many through: assocation" with scoped conditionals in the through model - ruby-on-rails

I have a schema that looks something like this:
create_table "customers" do |t|
t.integer "customer_number"
end
create_table "past_payments" do |t|
t.integer "customer_number"
t.datetime "transaction_date"
t.integer "arbitrary_sequence_number"
end
create_table "payment_details" do |t|
t.datetime "transaction_date"
t.integer "arbitrary_sequence_number"
end
TL;DR from the schema - A Customer is associated with a past_payment through a primary/foreign key. And a PastPayment is associated with a single PaymentDetail when their transaction_date AND arbitrary_sequence_number are equal. Payments and Details have no formal primary/foreign key relationship.
That gives me the following ActiveRecord models:
class Customer < ActiveRecord::Base
has_many :past_payments, foreign_key: :customer_number, primary_key: :customer_number
has_many :payment_details, through: :past_payments # unfortunately, broken šŸ˜¢
end
class PastPayment < ActiveRecord::Base
has_one :payment_detail, ->(past_payment) {
where(arbitrary_sequence_number: past_payment.arbitrary_sequence_number)
}, foreign_key: :transaction_date, primary_key: :transaction_date
end
Since a Customer has_many :past_payments and a PastPayment has_one :payment_detail, I would think there's an association that can be defined such that a Customer has_many :payment_details, through: :past_payments, but I can't get that to work because of the scope defined on the has_one :payment_detail association.
Specifically, calling Customer.payment_details raises the NoMethodError: undefined method 'arbitrary_sequence_number' for #<Customer:0x2i8asdf3>. So it would seem the Customer is getting passed to my scope as opposed to the PastPayment.
Is it possible to define the has_many :payment_details association on the Customer? Am I doing something wrong?
To be clear, I'd like to be able to say Customer.where(some_conditions).includes(:payment_details) and execute just the two queries so if there's a way to accomplish that without associations, I'm open to it.
Note: I can't change this database. It's a database some other application writes to, and I need to read from it.
Unrelated to my question, here's the workaround I'm currently working with. If there is no way to properly use associations, I'd be happy to have this solution critiqued:
class Customer < ActiveRecord::Base
attr_writer :payment_details
def payment_details
#payment_details ||= Array(self).with_payment_details.payment_details
end
module InjectingPaymentData
def with_payment_details
results = self.to_a
return self unless results.first.is_a?(Customer)
user_ids = results.collect(&:id)
# i've omitted the details of the query, but the idea is at the end of it
# we have a hash with the customer_number as a key pointing to an array
# of PaymentDetail objects
payment_details = PaymentDetails.joins().where().group_by(&:customer_number)
results.each do |customer|
customer.payment_details = Array(payment_details[customer.customer_number])
end
end
end
end
ActiveRecord::Relation.send(:include, Customer::InjectingPaymentData)
Array.send(:include, Customer::InjectingPaymentData)
And with that I can do things like the following with minimal querying:
#customers = Customer.where(id: 0..1000).with_payment_details
#customers.each { |c| do_something_with_those_payment_details }
Problems with that approach?

You can leverage this gem to better handle "composite primary keys" in ActiveRecord: https://github.com/composite-primary-keys/composite_primary_keys
class Customer < ActiveRecord::Base
has_many :past_payments, foreign_key: :customer_number, primary_key: :customer_number
has_many :payment_details, through: :past_payments # this works now
end
class PastPayment < ActiveRecord::Base
has_one :payment_detail, foreign_key: [:transaction_date, :arbitrary_sequence_number],
primary_key: [:transaction_date, :arbitrary_sequence_number]
end

Related

How to use one foreign key for two models in Ruby on Rails?

Here is my migration file
class CreateTestRevisions < ActiveRecord::Migration
def change
create_table :test_revisions do |t|
t.integer :user_id
t.integer :test_id
t.integer :question_id
t.integer :test_type
t.timestamps null: false
end
end
end
I have two models: OnlineTest & AllindiaTest.
How can I relate the t.integer :test_id to these two models?
You can set up a polymorphic association like this:
class OnlineTest < ActiveRecord::Base
has_many :test_revisions, as: :testable
end
class AllindiaTest < ActiveRecord::Base
has_many :test_revisions, as: :testable
end
class TestRevision < ActiveRecord::Base
belongs_to :testable, polymorphic: true
end
And make sure you have two columns in test_revisions table:
testable_id: integer
testable_type: string
testable_id will store id of OnlineTest or AllindiaTest.
testable_type will store OnlineTest or AllindiaTest string.
So if you have a test_devision, you can use test_devision.testable to get OnlineTest or AllindiaTest, depend on testable_type.
More information about Polymorphich assocication.
You can use foreign_key option to specify the same key for two models like:
class TestRevision < ApplicationRecord
belongs_to :online_test, foreign_key: :test_id
belongs_to :allindia_test, foreign_key: :test_id
end
Or the best way I think is to treat both of these objects as one through delegate:
delegate :allindia_test, to: :online_test
This way, you only need to have one association: online_test, and when you call, allindia_test, the call will be delegated to online_test.
Edit:
Well, if you would like to have two different relations, you would need two different ids in that case: online_test_id & all_india_test_id.
class TestRevision < ApplicationRecord
belongs_to :online_test
belongs_to :all_india_test
end

Is it advisable to use :foreign_key in my migrations rather than just adding user_id?

I am using rails 4.2, I just want to know if there would be any difference if I use the :foreign_key keyword in my migrations rather than just adding a user_id column to add relationship to my models ?
YES
The key difference is not on the application layer but on the database layer - foreign keys are used to make the database enforce referential integrity.
Lets look at an example:
class User < ActiveRecord::Base
has_many :things
end
class Thing < ActiveRecord::Base
belongs_to :user
end
If we declare things.user_id without a foreign key:
class CreateThings < ActiveRecord::Migration
def change
create_table :things do |t|
t.integer :user_id
t.timestamps null: false
end
end
end
ActiveRecord will happily allow us to orphan rows on the things table:
user = User.create(name: 'Max')
thing = user.things.create
user.destroy
thing.user.name # Boom! - 'undefined method :name for NilClass'
While if we had a foreign key the database would not allow us to destroy user since it leaves an orphaned record.
class CreateThings < ActiveRecord::Migration
def change
create_table :things do |t|
t.belongs_to :user, index: true, foreign_key: true
t.timestamps null: false
end
end
end
user = User.create(name: 'Max')
thing = user.things.create
user.destroy # DB says hell no
While you can simply regulate this with callbacks having the DB enforce referential integrity is usually a good idea.
# using a callback to remove associated records first
class User < ActiveRecord::Base
has_many :things, dependent: :destroy
end

undefined column in rails many to many

So i defined a has_and_belongs_to_many relationship between my 2 models as shown below
class Client < ActiveRecord::Base
attr_accessible :first_name, :last_name, :email
has_and_belongs_to_many :themes, class_name: 'XY::Theme'
end
class XY::Theme < ActiveRecord::Base
has_and_belongs_to_many :clients
end
then i defined my join table from the active record rails guide like this
class CreateClientsThemesJoinTable < ActiveRecord::Migration
def change
create_table :clients_xy_themes, id: false do |t|
t.integer :client_id
t.integer :xy_theme_id
end
add_index :clients_xy_themes, :client_id
add_index :clients_xy_themes, :xy_theme_id
end
end
but when i try to access the themes from clients table in rails console, iget this error
PG::UndefinedColumn: ERROR: column clients_xy_themes.theme_id does not exist
LINE 1: ...ER JOIN "clients_xy_themes" ON "xy_themes"."id" = "clients_v...
why is this happening? My migration specifically stated the keys on the themes table , but its trying to access a column that doesnt exist
The convention is, if your relation name is themes, then the foreign key name would be theme_id.
You could define the relation like:
has_and_belongs_to_many :xy_themes, class_name: 'XY::Theme'
Or you have to define the association_foreign_key option.
has_and_belongs_to_many :themes, class_name: 'XY::Theme', association_foreign_key: 'xy_theme_id'

Combining polymorphic association with many-to-many association... is this setup right?

I have a Genre model, and I want both videos to have many genres and profiles to have many genres. I also want genres to have many videos and genres to have many profiles. I understand the polymorphic and join table stuff, so I'm wondering if my code below will work as I intend it to. Also, I'd appreciate any advice on how to access things in my controller and views.
This is what I envision that the join table should look like (I don't think I need an elaborate :has :through association because all I need in the join table are the associations and nothing else, so the table won't have a model):
genres_videos_profiles:
-----------------------------------------------------
id | genre_id | genre_element_id | genre_element_type
Here's my genre.rb:
has_and_belongs_to_many :genre_element, :polymorphic => true
Here's video.rb:
has_and_belongs_to_many :genres, :as => :genre_element
Here's profile.rb:
has_and_belongs_to_many :genres, :as => :genre_element
Will this work as I intend it to? I'd like some feedback.
As far as I know HABTM associations canĀ“t be polymorphic, I couldnĀ“t find an example like yours in the API documentation. If you want only join tables, your code could look like this:
class Genre
has_and_belongs_to_many :videos
has_and_belongs_to_many :profiles
end
class Video
has_and_belongs_to_many :genres
end
class Profile
has_and_belongs_to_many :genres
end
And access it like Mike already wrote:
#genre.profiles
#profile.genres
#genre.videos
#video.genres
Migrations (for join tables only):
class CreateGenresVideosJoinTable < ActiveRecord::Migration
def self.up
create_table :genres_videos, {:id => false, :force => true} do |t|
t.integer :genre_id
t.integer :video_id
t.timestamps
end
end
def self.down
drop_table :genres_videos
end
end
class CreateGenresProfilesJoinTable < ActiveRecord::Migration
def self.up
create_table :genres_profiles, {:id => false, :force => true} do |t|
t.integer :genre_id
t.integer :profile_id
t.timestamps
end
end
def self.down
drop_table :genres_profiles
end
end
I think that has_and_belongs_to_many can be a bit difficult to follow when it comes to polymorphic (if it even works). So if you want to do the polymorhpic thing, then you can't use any "through" syntax:
class Genre < ActiveRecord::Base
has_many :genres_videos_profiles
end
class GenresVideosProfile
belongs_to :genre
belongs_to :genre_element, :polymorphic => true
scope :videos, where(:genre_element_type => "Video")
scope :profiles, where(:genre_element_type => "Profile")
end
And then you use it like:
# All genre elements
#genre.genres_videos_profiles.each do |gvp|
puts gvp.genre_element.inspect
end
# Only video genre elements
#genre.genres_videos_profiles.videos.each do |gvp|
puts gvp.genre_element.inspect
end
Check out that: http://blog.hasmanythrough.com/2006/4/3/polymorphic-through
For me it was perfect and clean!

Scaffolding ActiveRecord: two columns of the same data type

Another basic Rails question:
I have a database table that needs to contain references to exactly two different records of a specific data type.
Hypothetical example: I'm making a video game database. I have a table for "Companies." I want to have exactly one developer and exactly one publisher for each "Videogame" entry.
I know that if I want to have one company, I can just do something like:
script/generate Videogame company:references
But I need to have both companies. I'd rather not use a join table, as there can only be exactly two of the given data type, and I need them to be distinct.
It seems like the answer should be pretty obvious, but I can't find it anywhere on the Internet.
Just to tidy things up a bit, in your migration you can now also do:
create_table :videogames do |t|
t.belongs_to :developer
t.belongs_to :publisher
end
And since you're calling the keys developer_id and publisher_id, the model should probably be:
belongs_to :developer, :class_name => "Company"
belongs_to :publisher, :class_name => "Company"
It's not a major problem, but I find that as the number of associations with extra arguments get added, the less clear things become, so it's best to stick to the defaults whenever possible.
I have no idea how to do this with script/generate.
The underlying idea is easier to show without using script/generate anyway. You want two fields in your videogames table/model that hold the foreign keys to the companies table/model.
I'll show you what I think the code would look like, but I haven't tested it, so I could be wrong.
Your migration file has:
create_table :videogames do |t|
# all your other fields
t.int :developer_id
t.int :publisher_id
end
Then in your model:
belongs_to :developer, class_name: "Company", foreign_key: "developer_id"
belongs_to :publisher, class_name: "Company", foreign_key: "publisher_id"
You also mention wanting the two companies to be distinct, which you could handle in a validation in the model that checks that developer_id != publisher_id.
If there are any methods or validation you want specific to a certain company type, you could sub class the company model. This employs a technique called single table inheritance. For more information check out this article: http://wiki.rubyonrails.org/rails/pages/singletableinheritance
You would then have:
#db/migrate/###_create_companies
class CreateCompanies < ActiveRecord::Migration
def self.up
create_table :companies do |t|
t.string :type # required so rails know what type of company a record is
t.timestamps
end
end
def self.down
drop_table :companies
end
end
#db/migrate/###_create_videogames
class CreateVideogames < ActiveRecord::Migration
create_table :videogames do |t|
t.belongs_to :developer
t.belongs_to :publisher
end
def self.down
drop_table :videogames
end
end
#app/models/company.rb
class Company < ActiveRecord::Base
has_many :videogames
common validations and methods
end
#app/models/developer.rb
class Developer < Company
developer specific code
end
#app/models/publisher.rb
class Publisher < Company
publisher specific code
end
#app/models/videogame.rb
class Videogame < ActiveRecord::Base
belongs_to :developer, :publisher
end
As a result, you would have Company, Developer and Publisher models to use.
Company.find(:all)
Developer.find(:all)
Publisher.find(:all)

Resources