Rails string as a foreign key - ruby-on-rails

I have a relation between User and Course (typical enrollment data). A User has_many Course and vice-versa (typical JOIN table scenario).
I am attempting to migrate my previous has_and_belongs_to_many relationship between these two models to a has_many :through relationship. My files currently look like:
class User < ActiveRecord::Base
has_and_belongs_to_many :courses
end
and
class Course < ActiveRecord::Base
has_and_belongs_to_many :users
end
and the table name that joins the two models is courses_users.
I now need to migrate this relationship to the has_many :through association, and also make the column type of user_id a string, as I want to use the g_number (string) attribute of User as the foreign key. Note: I don't care about the performance difference between int and varchar/string.
The short and simple problem is that I need users.g_number to reference enrollments.user_id as a foreign key, and both are strings.
My attempt at a migration and model rework is this:
class User < ActiveRecord::Base
has_many :enrollment
has_many :courses, :through => :enrollment
end
and
class Course < ActiveRecord::Base
has_many :enrollment
has_many :users, :through => :enrollment
end
lastly
class Enrollment < ActiveRecord::Base
belongs_to :course
belongs_to :user
end
then the migration
class ChangeUserIdJoin < ActiveRecord::Migration
def self.up
rename_table :courses_users, :enrollments
end
def self.down
rename_table :enrollments, :courses_users
end
end
Everything works fine here. I can do queries like User.courses and Course.users. But now I want to change the type of the user_id column in the join table to a string so that I can store the g_number (string attribute on User) and join on that instead of the serial id column of User.
When I attempt to change the user_id column type to string in the migration:
class ChangeUserIdJoin < ActiveRecord::Migration
def self.up
change_column :courses_users, :user_id, :string
rename_table :courses_users, :enrollments
end
def self.down
rename_table :enrollments, :courses_users
change_column :courses_users, :user_id, :integer
end
end
the queries Course.users and User.courses start failing (below from Rails console). User.courses returns an empty array (whereas before there are multiple Course objects), and Course.users throws an exception because of mismatched column types (which obviously makes sense):
u = User.take
User Load (0.9ms) SELECT "users".* FROM "users" LIMIT 1
=> #<User id: 1, username: "director", g_number: "g00000000", password_digest: "$2a$10$dvcOd3rHfbcR1Rn/D6VhsOokj4XiIkQbHxXLYjy5s4f...", created_at: "2016-01-06 01:36:00", updated_at: "2016-01-06 01:36:00", first_name: "Director", last_name: "", role: 0, registered: true>
2.1.5 :002 > u.courses
Course Load (0.9ms) SELECT "courses".* FROM "courses" INNER JOIN "enrollments ON "courses"."id" = "enrollments"."course_id" WHERE "enrollments"."user_id" = $1 [["user_id", 1]]
=> #<ActiveRecord::Associations::CollectionProxy []>
2.1.5 :003 > c = Course.take
Course Load (0.7ms) SELECT "courses".* FROM "courses" LIMIT 1
=> #<Course id: 12754, year: 2015, semester: 0, department: 7, course: 101, section: 1, name: "SPA 101 01 - Elementary Spanish I">
2.1.5 :004 > c.users
PG::UndefinedFunction: ERROR: operator does not exist: integer = character varying
LINE 1: ... "users" INNER JOIN "enrollments" ON "users"."id" = "enrollm...
I need to be able to join on enrollments.user_id = users.g_number. What do I need to do in order to change the user_id column to a string type in the Enrollment model/table, and still be able to do Active Record queries like User.courses and Course.users?

Try by specifying the foreign and primary keys in enrollments model, like this
belongs_to :user foreign_key: :user_id, primary_key: :g_number

Related

Why is my Rails 7 Join Table not being created until I enter console commands?

I created a table for items and a table for jobs. I then created a join table, Items Jobs.
This is my migration for the join table and the models:
class CreateItemsJobs < ActiveRecord::Migration[7.0]
def change
create_table :items_jobs, id: false do |t|
t.belongs_to :item
t.belongs_to :job
t.timestamps
end
end
end
class Item < ApplicationRecord
belongs_to :part
belongs_to :employee, optional: true
has_and_belongs_to_many :jobs
end
class Job < ApplicationRecord
belongs_to :client
belongs_to :employee
has_and_belongs_to_many :items
end
class ItemsJobs < ApplicationRecord
belongs_to :item
belongs_to :job
end
I then migrate successfully...
rails db:migrate ==>
== 20220210032352 CreateItemsJobs: migrating ==================================
-- create_table(:items_jobs, {:id=>false})
-> 0.0085s
== 20220210032352 CreateItemsJobs: migrated (0.0086s) =========================
But if I try to seed, I get an error. If I run my rails console, I can't add to the table until I attempt to view a table that doesn't exist.
rails c ==>
Loading development environment (Rails 7.0.1)
2.7.4 :001 > ItemsJobs.all
Traceback (most recent call last):
(irb):1:in `<main>': uninitialized constant ItemsJobs (NameError)
Did you mean? ItemJob
2.7.4 :002 > ItemJob.all
Traceback (most recent call last):
(irb):2:in `<main>': uninitialized constant ItemJob (NameError)
Did you mean? ItemsJobs
2.7.4 :003 > ItemsJobs.all
ItemsJobs Load (0.4ms) SELECT "items_jobs".* FROM "items_jobs"
=> []
2.7.4 :004 > ItemsJobs.create(item_id: 1, job_id: 1)
TRANSACTION (0.2ms) BEGIN1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
Job Load (0.3ms) SELECT "jobs".* FROM "jobs" WHERE "jobs"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
ItemsJobs Create (0.6ms) INSERT INTO "items_jobs" ("item_id", "job_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4) [["item_id", 1], ["job_id", 1], ["created_at", "2022-02-10 15:23:44.127164"], ["updated_at", "2022-02-10 15:23:44.127164"]]
TRANSACTION (1.1ms) COMMIT
=>
#<ItemsJobs:0x00007f33c0aa7a80
item_id: 1,
job_id: 1,
created_at: Thu, 10 Feb 2022 15:23:44.127164000 UTC +00:00,
updated_at: Thu, 10 Feb 2022 15:23:44.127164000 UTC +00:00>
What is going wrong here? Why can't I view/add to the ItemsJobs table until I've attempted to view the suggested, non-existent ItemJob table?
You're mixing up has_and_belongs_to_many and has_many through: and the weird autoloading behavior is most likely because the naming scheme is throwing off how ActiveRecord maps classes to database tables through convention over configuration. The rule of thumb is that ActiveRecord expects models (class names) to be singular and tables to be plural.
HABTM assocations don't use a model for the join table. Rather its just a "headless" assocation where you just implicitly add/remove rows through the assocations on each end. HABTM is really only good for the case where you know that you won't need to query the table directly or access any additional columns - in other words it's quite useless outside of that niche. HABTM is the only place in ActiveRecord where you use the plural_plural naming scheme for tables - using that scheme with a model will cause constant lookup errors unless you explicitly configure everything.
If you want to setup an assocation with a join model (I would recommend this) you need to name the class and tables correctly and use an indirect assocation:
class CreateItemJobs < ActiveRecord::Migration[7.0]
def change
# table name should be singular_plural
create_table :item_jobs do |t|
t.belongs_to :item
t.belongs_to :job
t.timestamps
end
end
end
# model class names should always be singular
class ItemJob < ApplicationRecord
belongs_to :item
belongs_to :job
end
class Item < ApplicationRecord
belongs_to :part
belongs_to :employee, optional: true
has_many :item_jobs
has_many :jobs, through: :item_jobs
end
class Job < ApplicationRecord
belongs_to :client
belongs_to :employee
has_many :item_jobs
has_many :items, through: :item_jobs
end
Since you probally haven't run the migration in production and don't have any meaningful data in the table you can roll the bad CreateItemsJobs migration back and delete it. rails g model ItemJob item:belongs_to job:belongs_to will create both the model and migration.
See:
https://guides.rubyonrails.org/active_record_basics.html#convention-over-configuration-in-active-record
https://guides.rubyonrails.org/association_basics.html#the-has-many-through-association

How can I directly update the join model in a has_many_through association?

I have a Rails app with two models, joined by a third:
class Food < ApplicationRecord
has_many :shopping_list_items
has_many :users, through: :shopping_list_items
end
class ShoppingListItem < ApplicationRecord
belongs_to :user
belongs_to :food
end
class User < ApplicationRecord
has_many :shopping_list_items
has_many :foods, through: :shopping_list_items
end
The middle model, ShoppingListItem, has a few extra attributes, including priority which I'd like to update directly.
So for instance, I'd like to do something like:
r = current_user.shopping_list_items.where(food_id: 1).first
r.priority = "urgent"
r.save
The object looks fine until I try to save it, when I get a SQL error:
ActiveRecord::StatementInvalid (PG::SyntaxError: ERROR: zero-length delimited identifier at or near """"
LINE 1: ...$1, "updated_at" = $2 WHERE "shopping_list_items"."" IS NULL
^
: UPDATE "shopping_list_items" SET "priority" = $1, "updated_at" = $2 WHERE "shopping_list_items"."" IS NULL):
I guess it's complaining about the absence of a primary key? Not sure how to fix this, since the rails docs say that join tables shouldn't have a primary key column...
I created the middle table with a migration like this, :
create_table :shopping_list_items, id: false do |t|
t.belongs_to :user
t.belongs_to :food
t.string :priority
t.integer :position
t.timestamps
end

First time trying to use a foreign key other than model_id in Rails

So I know usually, a belongs_to associates models together based on the id column, but in my case I want to associate it by the token column instead.
For example:
#app/models/user.rb
class User < ApplicationRecord
has_many :test_results
end
and
#app/models/test_result.rb
class TestResult < ApplicationRecord
belongs_to :user
end
The User model has a column named token, and so when I create a new entry in TestResult that has the same token as what appears in User, I want the TestResult to be associated to that User.
I tried this in the model form:
#app/models/test_result.rb
class TestResult < ApplicationRecord
belongs_to :user, foreign_key: "token"
end
but when I go create a new test result, I can see that ActiveRecord is still looking for the id field that matches, instead of token.
2.5.1 :001 > TestResult.create(token: "Hello")
(6.2ms) SET NAMES utf8, ##SESSION.sql_mode = CONCAT(CONCAT(##sql_mode, ',STRICT_ALL_TABLES'), ',NO_AUTO_VALUE_ON_ZERO'), ##SESSION.sql_auto_is_null = 0, ##SESSION.wait_timeout = 2147483
(0.2ms) BEGIN
User Load (0.3ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 0 LIMIT 1
(0.2ms) ROLLBACK
=> #<TestResult id: nil, token: "Hello", created_at: nil, updated_at: nil>
Here's what my migration files look like:
class TestResults < ActiveRecord::Migration[5.1]
def change
create_table :test_results do |t|
t.string :token
t.timestamps
end
end
end
and
class User < ActiveRecord::Migration[5.1]
def change
create_table :users do |t|
t.belongs_to :company, foreign_key: true
t.belongs_to :platform, foreign_key: true
t.string :token
t.timestamps
end
end
end
I know I am using the foreign_key wrong, but I'm not sure how.
Not sure if this is the best solution, but just adding the primary_key function to the TestResult model in my case worked.
#app/models/test_result.rb
class TestResult < ApplicationRecord
belongs_to :user, foreign_key: "token", primary_key: "token"
end

Rake task foreign key assignment in a many to many relationship

I have two tables: Group and Keyword. They have a many-to-many relationship via another table called Assignments. I have a rake task where I generate random keywords, save the keyword to the Keyword's table and assign it to the current group's keywords. The code looks like this (I used a similar syntax to what I use in the console; however, I am not sure about it's correctness):
Group.populate 30 do |group|
Faker::Lorem.words(rand(3..7)).each do |key|
k = Keyword.create(name: key)
group.keywords << k
end
end
The << is returning this error: NoMethodError: undefined method<<'`.
If I ran the inner two lines in the console I get what I expect (note: I simplified the below models so you will see more keys in the log, but ignore that):
(0.1ms) begin transaction
SQL (2.0ms) INSERT INTO "keywords" ("name", "created_at", "updated_at") VALUES (?, ?, ?) [["name", "bla"], ["created_at", "2015-03-14 18:25:29.179793"], ["updated_at", "2015-03-14 18:25:29.179793"]]
(1.2ms) commit transaction
=> #<Keyword id: 1, name: "bla", created_at: "2015-03-14 18:25:29", updated_at: "2015-03-14 18:25:29">
(0.2ms) begin transaction
SQL (1.8ms) INSERT INTO "assignments" ("group_id", "keyword_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["group_id", 1], ["keyword_id", 1], ["created_at", "2015-03-14 18:26:53.650257"], ["updated_at", "2015-03-14 18:26:53.650257"]]
(0.8ms) commit transaction
=> #<ActiveRecord::Associations::CollectionProxy [#<Keyword id: 1, name: "bla", created_at: "2015-03-14 18:25:29", updated_at: "2015-03-14 18:25:29">]>
The Group model looks like this:
class Group < ActiveRecord::Base
has_many :assignments, dependent: :destroy
has_many :keywords, through: :assignments, dependent: :destroy
end
The Keyword model looks like that:
class Keyword < ActiveRecord::Base
has_many :assignments
has_many :groups, through: :assignments
end
And the Assignment model looks like this:
class Assignment < ActiveRecord::Base
belongs_to :group
belongs_to :keyword
end
How can I modify this code to work as expected?
TRy this
class Group < ActiveRecord::Base
has_many :assignments, dependent: :destroy
has_many :active_assignments, dependent: :destroy, class_name: "Assignment"
has_many :keywords, through: :active_assignments, source: :keyword
class Keyword < ActiveRecord::Base
has_many :assignments, dependent: :destroy
has_many :groups, through: :assignments
end
class Assignment < ActiveRecord::Base
belongs_to :group
belongs_to :keyword
end
Group.populate 30 do |group|
Faker::Lorem.words(rand(3..7)).each do |key|
group.keywords << Keyword.create(name: key)
end
end
I found the problem in my program. Even though it was a stupid mistake from me, I will leave the question and the error reported in case anybody fall into the same issue in the future.
I had this error because I had a field in my Group table which is named keywords, which of course conflicted with the Keywords table (which has many-to-many relation to Groups). I had this field from before I decided to create the many-to-many relation, and I forgot to remove it. Then, of course when you type something like this command group.keywords << Keyword.create(name: key) you will get this weird error: NoMethodError: undefined method<<.
However, fixing this mistake will not make the rake task run. This is because Group.populate returns a Populator object. If you run this task you will get this error: NoMethodError: undefined method keywords for #<Populator::Record:0x007fa1ee78fba8>. To fix that issue I had to assign the foreign keys manually in the task like so:
Group.populate 30 do |group|
Faker::Lorem.words(rand(3..7)).each do |key|
k = Keyword.create(name: key)
a = Assignment.create(keyword_id: k.id, group_id: group.id)
end
end
which will run as expected.

Rails 2.3.8 Association Problem has_many belongs_to

I'm new to Rails. I have two models, Person and Day.
class Person < ActiveRecord::Base
has_many :days
end
class Day < ActiveRecord::Base
belongs_to :person
has_many :runs
end
When I try to access #person.days I get an SQL error:
$ script/consoleLoading development environment (Rails 2.3.8)
ree-1.8.7-2010.02 > #person = Person.first
=> #<Person id: 1, first_name: "John", last_name: "Smith", created_at: "2010-08-29 14:05:50", updated_at: "2010-08-29 14:05:50"> ree-1.8.7-2010.02
> #person.days
ActiveRecord::StatementInvalid: SQLite3::SQLException: no such column: days.person_id: SELECT * FROM "days" WHERE ("days".person_id = 1)
I setup the association between the two before running any migrations, so I don't see why this has not been setup correctly.
Any suggestions?
Telling your model about the association doesn't set up the foreign key in the database - you need to create an explicit migration to add a foreign key to whichever table is appropriate.
For this I'd suggest:
script/generate migration add_person_id_to_days person_id:integer
then take a look at the migration file it creates for you to check it's ok, it should be something like this:
class AddPersonIdToDays < ActiveRecord::Migration
def self.up
add_column :days, :person_id, :integer
end
def self.down
remove_column :days, :person_id
end
end
Run that and try the association again?

Resources