How to search group_by with association in Rails - ruby-on-rails

I don't know how I describe this question, at first I want show my model which maintaining a relation like below
category.rb
class Category < ApplicationRecord
has_many :job_categories, dependent: :destroy
has_many :jobs, through: :job_categories
end
job.rb
class Job < ApplicationRecord
has_many :job_categories, dependent: :destroy
has_many :categories, through: :job_categories
end
job_category.rb
class JobCategory < ApplicationRecord
belongs_to :category, counter_cache: :jobs_count
belongs_to :job
end
schema.rb
create_table "categories", force: :cascade do |t|
t.string "name"
t.string "parent"
end
the parent is a column which maintain the group like Technology and under this ruby,rails,programming etc which is Technology related.
Below is my query for showing group by category
Category.select(:id, :name, :parent).group_by{|p| p.parent}
and it's showing like this
Technology
ruby
rails
etc
Now I want to show all jobs in group by Technology, I have a query for this like
Job.joins(:categories).where('lower(categories.parent) LIKE lower(?)', "%#{params[:parent]}%")
and it's showing wrong output like if I have only one job which categories is ruby,rails then this one job is showing two times, one for ruby and one for rails.
Thanks

Your associations are correct, you can retrieve all unique jobs for some categories by following:
Job.joins(:job_categories).joins(:categories).where('lower(categories.parent) LIKE lower(?)', "%#{params[:parent]}%").distinct
This will join the jobs with the intermediate table job_categories and jobs on relevant keys and where clause will then allow you to be selective on what you want to retrieve.
SELECT DISTINCT "jobs" .*
FROM "jobs" INNER
JOIN "job_categories" ON "job_categories" ."job_id" = "jobs" ."id" INNER
JOIN "job_categories" "job_categories_jobs_join" ON "job_categories_jobs_join" ."job_id" = "jobs" ."id" INNER
JOIN "categories" ON "categories" ."id" = "job_categories_jobs_join" ."category_id"
WHERE
(
lower ( categories.parent ) LIKE lower ( "Technology" ) )
Update:
Actually, we don't need to have explicit join to job_categories either, the following should suffice:
Job.joins(:categories).where('lower(categories.parent) LIKE lower(?)', "%#{params[:parent]}%").distinct
SELECT DISTINCT "jobs".* FROM "jobs" INNER JOIN "job_categories" ON "job_categories"."job_id" = "jobs"."id" INNER JOIN "categories" ON "categories"."id" = "job_categories"."category_id" WHERE (lower ( categories.parent ) LIKE lower ( "Technology" ))

Just few other options to fetch and group records with association has_many_through:
# Filtering by query
Job.joins(:categories).select('jobs.id, jobs.name, categories.parent').where('lower(categories.parent) LIKE lower(?)', "Technology").distinct.inspect
# => #<ActiveRecord::Relation [#<Job id: 1, name: "Developer">, #<Job id: 2, name: "Debugger">]>
# Grouping by categories.parent, return a hash
Job.joins(:categories).select('jobs.id, jobs.name, categories.parent').all.distinct.group_by(&:parent)
# => {"Technology"=>[#<Job id: 1, name: "Developer">, #<Job id: 2, name: "Debugger">], "Mechanics"=>[#<Job id: 3, name: "Technic">]}
# Accessing the hash by key
Job.joins(:categories).select('jobs.id, jobs.name, categories.parent').all.distinct.group_by(&:parent)["Technology"]
#=> [#<Job id: 1, name: "Developer">, #<Job id: 2, name: "Debugger">]

Related

Activerecord, how to chain multiple condition on a join table

I'm trying to find a way to create a query for my product database.
I have these models
class Category < DatabaseProducts::Base
has_many :category_searches
has_many :products
end
class Product < DatabaseProducts::Base
belongs_to :category
has_many :products_features
end
class Feature < DatabaseProducts::Base
has_many :products, through: :product_features
has_many :product_features
end
class ProductFeature < DatabaseProducts::Base
belongs_to :feature
belongs_to :product
end
class CategorySearch < DatabaseProducts::Base
belongs_to :category
end
Basically is a product database, and every product has some features, and values are stored in the ProductFeature join table.
Here is the structure, presentional_value is for the view, raw_value is for the search
create_table "product_features", force: :cascade do |t|
t.string "raw_value"
t.string "presentional_value"
t.integer "product_id"
t.integer "feature_id"
t.boolean "searchable", default: false
t.index ["feature_id"], name: "index_product_features_on_feature_id"
t.index ["product_id"], name: "index_product_features_on_product_id"
end
I have a Vue frontend in this product database, and I have multiple searches. To create the search field I create the category_searches table.
create_table "category_searches", force: :cascade do |t|
t.integer "category_id"
t.integer "feature_id"
t.string "name"
t.string "search_type"
t.string "search_options", default: [], array: true
t.index ["category_id"], name: "index_category_searches_on_category_id"
t.index ["feature_id"], name: "index_category_searches_on_feature_id"
end
Every night, when I import the new products in my database, I create new records or I update this table: for every searchable feature I store every possible searchable value.
For the TV Category, for example, in this table I have
category_id: 5
feature_id: 124
search_type: boolean
values: ["Yes","No"]
category_id: 5
feature_id: 235
search_type: options
values: ["OLED","LCD","QLED"]
In my Vue Frontend, for every category, I use the records in this table to draw the search interface, so when I select something the frontend send a request to my search API with these parameters:
category_id: 5
search_options: {"124" => "Yes", "235" => "OLED" ...}
Basically I have to search every product with category_id=5 where search_options.
Here I stop: I don't know how build the query.
I know that I have to join the products table and the products_features table.
And I know how to ask to Activerecord
"Find Products where raw_value == ?" or "Find Products where feature_id= ?"
It's a simple chained where. But I don't know how to ask ActiveRecord:
"Find Products where ProductFeature with feature_id=124 has the raw_value of "Yes" and where the feature_id=235 has the raw_value of "OLED" and... "
An AND clause won't really give you the result you want. What you want is an to use an OR clause and GROUP and HAVING:
f_id = ProductFeature.arel_table[:feature_id]
raw = ProductFeature.arel_table[:raw_value]
Product.joins(:product_features)
.where(
f_id.eq(124).and(raw.eq("Yes")).or(
f_id.eq(12345).and(raw.eq("No"))
)
)
.group("products.id")
.having(Arel.star.count.eq(2))
This results in the following query:
SELECT "products".*
FROM "products"
INNER JOIN "product_features"
ON "product_features"."product_id" = "products"."id"
WHERE ( "product_features"."feature_id" = 123
AND "product_features"."raw_value" = 'Yes'
OR "product_features"."feature_id" = 12345
AND "product_features"."raw_value" = 'No' )
GROUP BY "products"."id"
HAVING ( count(*) = 2 )
LIMIT ?
Which returns all products that have at least two matches in the join table.
You might want to use a JSON/JSONB column instead of a string column for the value storage. This will help you mitigate one of the biggest problems with the EAV pattern which is the headaches of typecasting everything into a string column.
On Postgres (and probably MySQL) you can use WHERE (columns) IN (values) to compose a simpler and more effective query:
class Product < ApplicationRecord
has_many :product_features
has_many :features, through: :product_features
def self.search_by_features(*pairs)
t = ProductFeature.arel_table
conditions = Arel::Nodes::In.new(
Arel::Nodes::Grouping.new( [t[:feature_id], t[:raw_value]] ),
pairs.map { |pair| Arel::Nodes::Grouping.new(
pair.map { |value| Arel::Nodes.build_quoted(value) }
)}
)
Product.joins(:product_features)
.where(
conditions
).group(:id)
.having(Arel.star.count.eq(pairs.length))
end
end
Usage:
Product.search_by_features([1, "Yes"], [2, "No"], [3, "Maybe"])
SQL query:
SELECT "products".*
FROM "products"
INNER JOIN "product_features"
ON "product_features"."product_id" = "products"."id"
WHERE
("product_features"."feature_id", "product_features"."raw_value")
IN
((1, 'Yes'),(2, 'No'),(3, 'Maybe'))
GROUP BY "products"."id"
HAVING ( COUNT(*) = 3) )
LIMIT $1
How about using left joins?
Product
.left_joins(:product_features)
.left_joins(:features)
.where(product_features: {feature_id: feature_id, raw_values: "YES"})
.where(features: {feature_id: feature_id, raw_values: "OLED"})
if there is more table that can be joined, just add left joins statement and where statement again
I don't think ActiveRecord supports such queries. You can use plain SQL, or Arel, or ransack, etc.
Cannot check at the moment, but Arel version may look similar to this:
products = Product.arel_table
result = params[:search_options].reduce(Product.where(category_id: 5)) do |scope, (feature_id, feature_value)|
product_feature_table = ProductFeature.arel_table.alias("product_feature_#{feature_id}")
join_clause = product_feature_table[:product_id].eq(products[:id]).
merge(product_feature_table[:raw_value].eq(feature_value))
scope.joins(product_feature_table.on(join_clause)
end

Two belong_to referring the same table + eager loading

First of all, based on this (Rails association with multiple foreign keys) I figured out how to make two belong_to pointing to the same table.
I have something like that
class Book < ApplicationRecord
belongs_to :author, inverse_of: :books
belongs_to :co_author, inverse_of: :books, class_name: "Author"
end
class Author < ApplicationRecord
has_many :books, ->(author) {
unscope(:where).
where("books.author_id = :author_id OR books.co_author_id = :author_id", author_id: author.id)
}
end
It's all good. I can do either
book.author
book.co_author
author.books
However, sometimes I need to eager load books for multiple authors (to avoid N queries).
I am trying to do something like:
Author.includes(books: :title).where(name: ["Lewis Carroll", "George Orwell"])
Rails 5 throws at me: "ArgumentError: The association scope 'books' is instance dependent (the scope block takes an argument). Preloading instance dependent scopes is not supported."
I am trying to figure out what I should do?
Should I go with many-to-many association? It sounds like a solution. However, it looks like it will introduce it's own problems (I need "ordering", meaning that I need explicitly differentiate between main author and co-author).
Just trying to figure out whether I am missing some simpler solution...
Why do you not use HABTM relation? For example:
# Author model
class Author < ApplicationRecord
has_and_belongs_to_many :books, join_table: :books_authors
end
# Book model
class Book < ApplicationRecord
has_and_belongs_to_many :authors, join_table: :books_authors
end
# Create books_authors table
class CreateBooksAuthorsTable < ActiveRecord::Migration
def change
create_table :books_authors do |t|
t.references :book, index: true, foreign_key: true
t.references :author, index: true, foreign_key: true
end
end
end
You can use eagerload like as following:
irb(main):007:0> Author.includes(:books).where(name: ["Lewis Carroll", "George Orwell"])
Author Load (0.1ms) SELECT "authors".* FROM "authors" WHERE "authors"."name" IN (?, ?) LIMIT ? [["name", "Lewis Correll"], ["name", "George Orwell"], ["LIMIT", 11]]
HABTM_Books Load (0.1ms) SELECT "books_authors".* FROM "books_authors" WHERE "books_authors"."author_id" IN (?, ?) [["author_id", 1], ["author_id", 2]]
Book Load (0.1ms) SELECT "books".* FROM "books" WHERE "books"."id" IN (?, ?) [["id", 1], ["id", 2]]
Try this:
Author.where(name: ["Lewis Carroll", "George Orwell"]).include(:books).select(:title)

Delete has_one through: association

I have
class Job < ApplicationRecord
has_one :user, through: :jobs_user
has_one :jobs_user, dependent: :destroy
end
and the model for the join_table looks like this:
class JobsUser < ApplicationRecord
belongs_to :job
belongs_to :user
end
The migration was:
create_join_table :jobs, :shops do |t|
t.index :job_id
end
When I create a job and try to delete it fails :
j = Job.create(user: User.last)
j.destroy!
Job Load (0.3ms) SELECT "jobs".* FROM "jobs" ORDER BY "jobs"."id" DESC LIMIT 1
(0.2ms) BEGIN
JobsShop Load (0.3ms) SELECT "jobs_shops".* FROM "jobs_shops" WHERE "jobs_shops"."job_id" = 21365 LIMIT 1 [["job_id", 21365]]
SQL (0.7ms) DELETE FROM "jobs_shops" WHERE "jobs_shops"."" = NULL [[nil, nil]]
(0.2ms) ROLLBACK
ActiveRecord::StatementInvalid: PG::SyntaxError: ERROR: zero-length delimited identifier at or near """"
LINE 1: DELETE FROM "jobs_shops" WHERE "jobs_shops"."" = NULL
^
: DELETE FROM "jobs_shops" WHERE "jobs_shops"."" = NULL
It seems I failed somewhere and it cannot find the column to destroy.
The answer can be found here : https://github.com/rails/rails/issues/25347#issuecomment-300067025
Active Record doesn't have built in support for composite primary keys
That means you can't manipulate a model whose corresponding table doesn't have a single-column primary key defined. That includes doing so through an association that uses said model.
So in my case, choosing create_join_table was not the right choice. Instead create a normal table.
create_table :users_jobs do |t|
t.integer :user_id
t.integer :job_id
# t.index :job_id
end

Has Many 'finder_sql' Replacement in Rails 4.2

I've got an association that needs a few joins / custom queries. When trying to figure out how to implement this the repeated response is finder_sql. However in Rails 4.2 (and above):
ArgumentError: Unknown key: :finder_sql
My query to do the join looks like this:
'SELECT DISTINCT "tags".*' \
' FROM "tags"' \
' JOIN "articles_tags" ON "articles_tags"."tag_id" = "tags"."id"' \
' JOIN "articles" ON "article_tags"."article_id" = "articles"."id"' \
' WHERE articles"."user_id" = #{id}'
I understand that this can be achieved via:
has_many :tags, through: :articles
However if the cardinality of the join is large (i.e. a user has thousands of articles - but the system only has a few tags) it requires loading all the articles / tags:
SELECT * FROM articles WHERE user_id IN (1,2,...)
SELECT * FROM article_tags WHERE article_id IN (1,2,3...) -- a lot
SELECT * FROM tags WHERE id IN (1,2,3) -- a few
And of course also curious about the general case.
Note: also tried using the proc syntax but can't seem to figure that out:
has_many :tags, -> (user) {
select('DISTINCT "tags".*')
.joins('JOIN "articles_tags" ON "articles_tags"."tag_id" = "tags"."id"')
.joins('JOIN "articles" ON "article_tags"."article_id" = "articles"."id"')
.where('"articles"."user_id" = ?', user.id)
}, class_name: "Tag"
ActiveRecord::StatementInvalid: PG::UndefinedColumn: ERROR: column tags.user_id does not exist
SELECT DISTINCT "tags".* FROM "tags" JOIN "articles_tags" ON "articles_tags"."tag_id" = "tags"."id" JOIN "articles" ON "article_tags"."article_id" = "articles"."id" WHERE "tags"."user_id" = $1 AND ("articles"."user_id" = 1)
That is it looks like it is trying to inject the user_id onto tags automatically (and that column only exists on articles). Note: I'm preloading for multiple users so can't use user.tags without other fixes (the SQL pasted is what I'm seeing using exactly that!). Thoughts?
While this doesn't fix your problem directly - if you only need a subset of your data you can potentially preload it via a subselect:
users = User.select('"users".*"').select('COALESCE((SELECT ARRAY_AGG(DISTINCT "tags"."name") ... WHERE "articles"."user_id" = "users"."id"), '{}') AS tag_names')
users.each do |user|
puts user[:tag_names].join(' ')
end
The above is DB specific for Postgres (due to ARRAY_AGG) but an equivalent solution probably exists for other databases.
An alternative option might be to setup a view as a fake join table (again requires database support):
CREATE OR REPLACE VIEW tags_users AS (
SELECT
"users"."id" AS "user_id",
"tags"."id" AS "tag_id"
FROM "users"
JOIN "articles" ON "users"."id" = "articles"."user_id"
JOIN "articles_tags" ON "articles"."id" = "articles_tags"."article_id"
JOIN "tags" ON "articles_tags"."tag_id" = "tags"."id"
GROUP BY "user_id", "tag_id"
)
Then you can use has_and_belongs_to_many :tags (haven't tested - may want to set to readonly and can remove some of the joins and use if you have proper foreign key constraints setup).
So my guess is you are getting the error when you try to access #user.tags since you have that association inside the user.rb.
So I think what happens is when we try to access the #user.tags, we are trying to fetch the tags of the user and to that rails will search Tags whose user_id matches with currently supplied user's id. Since rails takes association name as modelname_id format by default, even if you don't have user_id it will try to search in that column and it will search (or add WHERE "tags"."user_id") no matter you want it to or not since ultimate goal is to find tags that are belongs to current user.
Of course my answer may not explain it 100%. Feel free to comment your thought or If you find anything wrong, let me know.
Short Answer
Ok, if I understand this correctly I think I have the solution, that just uses the core ActiveRecord utilities and does not use finder_sql.
Could potentially use:
user.tags.all.distinct
Or alternatively, in the user model change the has_many tags to
has_many :tags, -> {distinct}, through: :articles
You could create a helper method in user to retrieve this:
def distinct_tags
self.tags.all.distinct
end
The Proof
From your question I believe you have the following scenario:
A user can have many articles.
An article belongs to a single user.
Tags can belong to many articles.
Articles can have many tags.
You want to retrieve all the distinct tags a user has associated with the articles they have created.
With that in mind I created the following migrations:
class CreateUsers < ActiveRecord::Migration
def change
create_table :users do |t|
t.string :name, limit: 255
t.timestamps null: false
end
end
end
class CreateArticles < ActiveRecord::Migration
def change
create_table :articles do |t|
t.string :name, limit: 255
t.references :user, index: true, null: false
t.timestamps null: false
end
add_foreign_key :articles, :users
end
end
class CreateTags < ActiveRecord::Migration
def change
create_table :tags do |t|
t.string :name, limit: 255
t.timestamps null: false
end
end
end
class CreateArticlesTagsJoinTable < ActiveRecord::Migration
def change
create_table :articles_tags do |t|
t.references :article, index: true, null:false
t.references :tag, index: true, null: false
end
add_index :articles_tags, [:tag_id, :article_id], unique: true
add_foreign_key :articles_tags, :articles
add_foreign_key :articles_tags, :tags
end
end
And the models:
class User < ActiveRecord::Base
has_many :articles
has_many :tags, through: :articles
def distinct_tags
self.tags.all.distinct
end
end
class Article < ActiveRecord::Base
belongs_to :user
has_and_belongs_to_many :tags
end
class Tag < ActiveRecord::Base
has_and_belongs_to_many :articles
end
Next seed the database with a lot of data:
10.times do |tagcount|
Tag.create(name: "tag #{tagcount+1}")
end
5.times do |usercount|
user = User.create(name: "user #{usercount+1}")
1000.times do |articlecount|
article = Article.new(user: user)
5.times do |tagcount|
article.tags << Tag.find(tagcount+usercount+1)
end
article.save
end
end
Finally in rails console:
user = User.find(3)
user.distinct_tags
results in following output:
Tag Load (0.4ms) SELECT DISTINCT `tags`.* FROM `tags` INNER JOIN `articles_tags` ON `tags`.`id` = `articles_tags`.`tag_id` INNER JOIN `articles` ON `articles_tags`.`article_id` = `articles`.`id` WHERE `articles`.`user_id` = 3
=> #<ActiveRecord::AssociationRelation [#<Tag id: 3, name: "tag 3", created_at: "2016-10-18 22:00:52", updated_at: "2016-10-18 22:00:52">, #<Tag id: 4, name: "tag 4", created_at: "2016-10-18 22:00:52", updated_at: "2016-10-18 22:00:52">, #<Tag id: 5, name: "tag 5", created_at: "2016-10-18 22:00:52", updated_at: "2016-10-18 22:00:52">, #<Tag id: 6, name: "tag 6", created_at: "2016-10-18 22:00:52", updated_at: "2016-10-18 22:00:52">, #<Tag id: 7, name: "tag 7", created_at: "2016-10-18 22:00:52", updated_at: "2016-10-18 22:00:52">]>
May be it is helpful to use eager_load to force ActiveRecord execute joins. It works as includes(:tags).references(:tags)
Here is a code snippet:
users.eager_load(:tags).map { |user| user.tag.inspect }
# equal to
users.includes(:tags).references(:tags).map { |user| user.tag.inspect }
Where users - is an ActiveRecord relation.
This code will hit a database at least twice:
Select only users ids (hopefully, not too many)
Select users with joins tags through article_tags avoiding
SELECT * FROM article_tags WHERE article_id IN (1,2,3...) -- a lot
You are on the right path with has_many :tags, through: :articles (or even better has_many :tags, -> {distinct}, through: :articles as Kevin suggests). But you should read a bit about includes vs preload vs eager_load. You are doing this:
User.preload(:tags).each {|u| ... }
But you should do this:
User.eager_load(:tags).each {|u| ... }
or this:
User.includes(:tags).references(:tags).each {|u| ... }
When I do that I get this query:
SELECT "users"."id" AS t0_r0,
"tags"."id" AS t1_r0,
"tags"."name" AS t1_r1
FROM "users"
LEFT OUTER JOIN "articles"
ON "articles"."user_id" = "users"."id"
LEFT OUTER JOIN "articles_tags"
ON "articles_tags"."article_id" = "articles"."id"
LEFT OUTER JOIN "tags"
ON "tags"."id" = "articles_tags"."tag_id"
But that is still going to send a lot of redundant stuff from the database to your app. This will be faster:
User.eager_load(:tags).distinct.each {|u| ... }
Giving:
SELECT DISTINCT "users"."id" AS t0_r0,
"tags"."id" AS t1_r0,
"tags"."name" AS t1_r1
FROM "users"
LEFT OUTER JOIN "articles"
ON "articles"."user_id" = "users"."id"
LEFT OUTER JOIN "articles_tags"
ON "articles_tags"."article_id" = "articles"."id"
LEFT OUTER JOIN "tags"
ON "tags"."id" = "articles_tags"."tag_id"
Doing just User.first.tags.map &:name gets me joins too:
SELECT DISTINCT "tags".*
FROM "tags"
INNER JOIN "articles_tags"
ON "tags"."id" = "articles_tags"."tag_id"
INNER JOIN "articles"
ON "articles_tags"."article_id" = "articles"."id"
WHERE "articles"."user_id" = ?
For more details, please see this github repo with an rspec test to see what SQL Rails is using.
There are three possible solutions:
1) Continue to use has_many associations
Fake user_id column by adding it to the selected columns.
class User < ActiveRecord::Base
has_many :tags, -> (user) {
select(%Q{DISTINCT "tags".*, #{user_id} AS user_id })
.joins('JOIN "articles_tags" ON "articles_tags"."tag_id" = "tags"."id"')
.joins('JOIN "articles" ON "article_tags"."article_id" = "articles"."id"')
.where('"articles"."user_id" = ?', user.id)
}, class_name: "Tag"
end
2) Add an instance method on the User class
If you are using tags for queries only and you haven't used it in joins you can use this approach:
class User
def tags
select(%Q{DISTINCT "tags".*})
.joins('JOIN "articles_tags" ON "articles_tags"."tag_id" = "tags"."id"')
.joins('JOIN "articles" ON "article_tags"."article_id" = "articles"."id"')
.where('"articles"."user_id" = ?', id)
end
end
Now user.tags behaves like an association for all practical purposes.
3) OTOH, using EXISTS might be performant than using distinct
class User < ActiveRecord::Base
def tags
exists_sql = %Q{
SELECT 1
FROM articles,
articles_tags
WHERE "articles"."user_id" = #{id} AND
"articles_tags"."article_id" = "article"."id" AND
"articles_tags"."tag_id" = "tags.id"
}
Tag.where(%Q{ EXISTS ( #{exists_sql} ) })
end
end

Rails string as a foreign key

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

Resources