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'
Related
recently I have a migration that adds a user_id column to the watch_events tables. and thus I want to change the watch_event models to handle belongs_to but with multiple approach
create_table 'users', force: :cascade do |t|
t.integer 'id'
t.integer 'customer_id'
end
create_table 'watch_events', force: :cascade do |t|
t.integer 'customer_id'
t.integer 'user_id'
end
previously
class WatchEvent < ApplicationRecord
belongs_to :user, foreign_key: :customer_id, primary_key: :customer_id
end
what I want:
if watch_event.customer_id is present, i want to use belongs_to :user, foreign_key: :customer_id, primary_key: :customer_id
if watch_event.customer_id is not present, i want to use normal belongs_to :user
how can I achieve this on the watch_event model?
I do not think that Rails supports 'fallback foreign keys' on associations. However, you can write a simple wrapper for your problem. First, relate your WatchEvent class twice to the user model, using your two keys and two 'internal' association names (:user_1 and :user_2). Then, add a 'virtual association reader' (user) and a 'virtual association setter' (user=(new_user)):
class WatchEvent < ApplicationRecord
belongs_to :user_1,
class_name: 'User',
foreign_key: :customer_id
belongs_to :user_2,
class_name: 'User',
foreign_key: :user_id
def user
user_1 || user_2
end
def user=(new_user)
self.user_1 = new_user
end
end
With this solution, the requirements "use customer_id to find user" and "use user_id as fallback foreign key if customer_id is nil or doesn't yield a result" is satisfied. It happens in the association reader method user. When there is a reader method, you'll need a setter method, which is user=(). Feel free to design the setter's internals as required, mine is just a suggestion.
BTW: You may need to re-add the declaration of the foreign primary_key. I omitted that for clarity.
If I understand your question correctly, then what you are looking for is a Polymorphic association.
If you see the code below, what it basically does is create two columns in the watch_events table, watcher_type and watcher_id. And the belongs_to :watcher then uses the watcher_type column to identify which model it should associate to,
create_table 'watch_events', force: :cascade do |t|
t.references 'watcher', polymorphic: true, null: false
end
class WatchEvent < ApplicationRecord
belongs_to :watcher, polymorphic: true
end
I am new to ROR, and now doing a course project related to books.
I have a "book" table with string 'isbn' as its primary key, and now trying to add another "comment" with reference to the book table with a foreign key 'isbn' to refer to the "book" table .
So my "models/comment.rb" looks like this:
class Comment < ApplicationRecord
belongs_to :book, references: :isbn, type: :string
end
And "models/book.rb" is:
class Book < ApplicationRecord
has_many :comments, dependent: :destroy
end
My "books" table looks like:
I would expect that the "comments" table generated will contain a column "isbn" (string) after db migration, but in fact the "comments" table generated contained a "book_id" (integer) instead.
How can I generate a foreign key to reference to the string primary key "isbn" in "book" table?
Start by setting up the foreign key column to be the right type and set the foreign key constraint to point to the right column:
class CreateComments < ActiveRecord::Migration[6.0]
def change
create_table :comments do |t|
t.string :title
t.string :note
t.references :book, type: :string, column_name: :isbn
t.timestamps
end
end
end
I would expect that the "comments" table generated will contain a
column "isbn" (string) after db migration, but in fact the "comments"
table generated contained a "book_id" (integer) instead.
Migrations are actually a lot simpler and dumber than you would think. Its just a DSL that creates SQL statements. Its not actually in any way aware of the other table so it has no way of knowing that books.isbn is a string column. It just uses the default type for add_reference which is :integer. Rails is driven by convention - not artificial intelligence.
If you want to call the column something else like book_isbn you have to do it in two steps instead:
class CreateComments < ActiveRecord::Migration[6.0]
def change
create_table :comments do |t|
t.string :title
t.string :note
t.string :book_isbn
t.timestamps
end
add_foreign_key :comments, :books,
column: "book_isbn", primary_key: "isbn"
end
end
Then setup the books model to use your non-conventional primary key:
class Book < ApplicationRecord
self.primary_key = :isbn
has_many :comments,
foreign_key: :book_isbn # default is book_id
end
On the comment side you need to configure the belongs_to association so that it points to books.isbn instead of books.id.
class Comment < ApplicationRecord
belongs_to :book, primary_key: :isbn
end
references is not a valid option for belongs_to.
ArgumentError (Unknown key: :references. Valid keys are: :class_name, :anonymous_class, :foreign_key, :validate, :autosave, :foreign_type, :dependent, :primary_key, :inverse_of, :required, :polymorphic, :touch, :counter_cache, :optional, :default)
I have to two models in the same namespace which have a habtm relation.
class Resource::Item < ApplicationRecord
has_and_belongs_to_many :resource_sets, foreign_key: 'resource_item_id', class_name: 'Resource::Set', table_name: 'resource_items_sets'
end
class Resource::Set < ApplicationRecord
has_and_belongs_to_many :resource_items, foreign_key: 'resource_set_id', class_name: 'Resource::Item', table_name: 'resource_items_sets'
end
The migration has been generated with rails g migration CreateJoinTableResourceItemsResourceSets resource_item resource_set
class CreateJoinTableResourceItemsResourceSets < ActiveRecord::Migration[5.2]
def change
create_join_table :resource_items, :resource_sets do |t|
# t.index [:resource_item_id, :resource_set_id]
# t.index [:resource_set_id, :resource_item_id]
end
end
end
So far everything looks great. The table resource_items_sets is being created with the two columns resource_item_id and resource_set_id.
This is the schema
create_table "resource_items_sets", id: false, force: :cascade do |t|
t.bigint "resource_item_id", null: false
t.bigint "resource_set_id", null: false
end
After creating an resource_item I get the following which is as expected.
pry(main)> Resource::Item.first.resource_sets
=> #<Resource::Set::ActiveRecord_Associations_CollectionProxy:0x3fdd08748004>
But doing the following throws an error. I was expecting 0.
pry(main)> Resource::Item.first.resource_sets.count
ActiveRecord::StatementInvalid: PG::UndefinedColumn: ERROR: column resource_items_sets.set_id does not exist
LINE 1: ...N "resource_items_sets" ON "resource_sets"."id" = "resource_...
^
: SELECT "resource_sets"."id" FROM "resource_sets" INNER JOIN "resource_items_sets" ON "resource_sets"."id" = "resource_items_sets"."set_id" WHERE "resource_items_sets"."resource_item_id" = $1 ORDER BY "resource_sets"."name" ASC
from /Users/username/.rvm/gems/ruby-2.6.0/gems/activerecord-5.2.2/lib/active_record/connection_adapters/postgresql_adapter.rb:677:in `async_prepare'
Caused by PG::UndefinedColumn: ERROR: column resource_items_sets.set_id does not exist
LINE 1: ...N "resource_items_sets" ON "resource_sets"."id" = "resource_...
Where does set_id come from when resource_set_id has been declare everywhere? How can I fix this issue? I want to keep the namespaces for both since I might end up creating items and set for more namespaces.
Thanks so much, guys!
You need to set the foreign keys on both sides of the join table because they can't be inferred in your case.
In this case the correct has_and_belongs_to_many calls should look like:
has_and_belongs_to_many :resource_items,
foreign_key: 'resource_set_id',
association_foreign_key: 'resource_item_id',
class_name: 'Resource::Item',
join_table: 'resource_items_sets'
end
and
class Resource::Item < ApplicationRecord
has_and_belongs_to_many :resource_sets,
foreign_key: 'resource_item_id',
association_foreign_key: 'resource_set_id',
class_name: 'Resource::Set',
join_table: 'resource_items_sets'
end
Note the added association_foreign_key option specifying the other foreign key in the join table.
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
I have the following two Models:
class Store < ActiveRecord::Base
belongs_to :person
end
class Person < ActiveRecord::Base
has_one :store
end
Here is the issue: I am trying to create a migration to create the foreign key within the people table. However, the column referring to the foreign key of Store is not named store_id as would be rails convention but is instead named foo_bar_store_id.
If I was following the rails convention I would do the migration like this:
class AddReferencesToPeople < ActiveRecord::Migration
def change
add_reference :people, :store, index: true
end
end
However this will not work because the column name is not store_id but is foo_bar_store_id. So how do I specify that the foreign key name is just different, but still maintain index: true to maintain fast performance?
in rails 5.x you can add a foreign key to a table with a different name like this:
class AddFooBarStoreToPeople < ActiveRecord::Migration[5.0]
def change
add_reference :people, :foo_bar_store, foreign_key: { to_table: :stores }
end
end
Inside a create_table block
t.references :feature, foreign_key: {to_table: :product_features}
In Rails 4.2, you can also set up the model or migration with a custom foreign key name. In your example, the migration would be:
class AddReferencesToPeople < ActiveRecord::Migration
def change
add_column :people, :foo_bar_store_id, :integer, index: true
add_foreign_key :people, :stores, column: :foo_bar_store_id
end
end
Here is an interesting blog post on this topic. Here is the semi-cryptic section in the Rails Guides. The blog post definitely helped me.
As for associations, explicitly state the foreign key or class name like this (I think your original associations were switched as the 'belongs_to' goes in the class with the foreign key):
class Store < ActiveRecord::Base
has_one :person, foreign_key: :foo_bar_store_id
end
class Person < ActiveRecord::Base
belongs_to :foo_bar_store, class_name: 'Store'
end
Note that the class_name item must be a string. The foreign_key item can be either a string or symbol. This essentially allows you to access the nifty ActiveRecord shortcuts with your semantically-named associations, like so:
person = Person.first
person.foo_bar_store
# returns the instance of store equal to person's foo_bar_store_id
See more about the association options in the documentation for belongs_to and has_one.
To expand on schpet's answer, this works in a create_table Rails 5 migration directive like so:
create_table :chapter do |t|
t.references :novel, foreign_key: {to_table: :books}
t.timestamps
end
EDIT: For those that see the tick and don't continue reading!
While this answer achieves the goal of having an unconventional foreign key column name, with indexing, it does not add a fk constraint to the database. See the other answers for more appropriate solutions using add_foreign_key and/or 'add_reference'.
Note: ALWAYS look at the other answers, the accepted one is not always the best!
Original answer:
In your AddReferencesToPeople migration you can manually add the field and index using:
add_column :people, :foo_bar_store_id, :integer
add_index :people, :foo_bar_store_id
And then let your model know the foreign key like so:
class Person < ActiveRecord::Base
has_one :store, foreign_key: 'foo_bar_store_id'
end
# Migration
change_table :people do |t|
t.references :foo_bar_store, references: :store #-> foo_bar_store_id
end
# Model
# app/models/person.rb
class Person < ActiveRecord::Base
has_one :foo_bar_store, class_name: "Store"
end
Under the covers add_reference is just delegating to add_column and add_index so you just need to take care of it yourself:
add_column :people, :foo_bar_store_id, :integer
add_index :people, :foo_bar_store_id
foreign key with different column name
add_reference(:products, :supplier, foreign_key: { to_table: :firms })
refer the documentation Docs