Rails 5 ActiveRecord Query - Possible to join 3 tables? - ruby-on-rails

Given the following models:
User: id
UserPosition: user_id, job_title_id
JobTitle: id | title
With Rails 5, how can I do something like:
current_user.job_title
What would I need to lookup UserPosition and then JobTitle to get the title?
Is this possible with one query?

You can do this through associations like this:
class User < ApplicationRecord
has_many :user_positions
has_many :job_titles, through: :user_positions
end
class UserPositions < ApplicationRecord
belongs_to :user
belongs_to :job_title
end
class JobTitle < ApplicationRecord
has_many :user_positions
has_many :users, through: :user_positions
end
Here's the documentation for a many to many relationship in Rails.

With your relationships defined as:
class User < ApplicationRecord
has_many :user_positions
end
class JobTitle < ApplicationRecord
has_many :user_positions
end
class UserPosition < ApplicationRecord
belongs_to :user
belongs_to :job_title
end
Then you can use joins, with both models job_title and user_position, through the user model, and knowing the user.id, so, then you can use pluck to get the needed attribute:
User.joins(user_positions: :job_title).where(id: 1).pluck('job_titles.title')
Which would give you an SQL query like:
SELECT job_titles.title
FROM "users"
INNER JOIN "user_positions"
ON "user_positions"."user_id" = "users"."id"
INNER JOIN "job_titles"
ON "job_titles"."id" = "user_positions"."job_title_id"
WHERE (users.id = 1)

Related

Shopping cart Active Record Associations

I am trying to implement a shopping cart within a Rails application. For this, I am trying to glue together these models:
user.rb
class User < ApplicationRecord
has_many :cart_contents
end
cart_content.rb:
class CartContent < ApplicationRecord
belongs_to :user
has_one :product
end
product.rb
class Product < ApplicationRecord
end
To test if this arrangement is working, I tried this in the console:
irb(main):006:0> cc = CartContent.new
=>
#<CartContent:0x000055679d802a28
...
irb(main):008:0> cc.user = User.find(1)
User Load (0.2ms) SELECT "users".* FROM [...]
=> #<User id: 1, email: "mail#example.com", ...
irb(main):010:0> cc.product = Product.find(1)
Product Load (0.1ms) SELECT "products".* FROM [...]
/[...]/activemodel-7.0.0/lib/active_model/attribute.rb:211:
in `with_value_from_database': can't write unknown attribute `cart_content_id` (ActiveModel::MissingAttributeError)
What am I missing here? Do I need to indicate a relationship to cart_content in product.rb?
One possible solution: you may want your CartContent to belongs_to :product.
class CartContent < ApplicationRecord
belongs_to :user
has_one :product # change this to belongs_to
end
cc.product = Product.find(1)
# .. can't write unknown attribute `cart_content_id` (ActiveModel::MissingAttributeError)
has_one will expect your products table to have a cart_content_id column.
See guide section 2.7 Choosing Between belongs_to and has_one
For this, you need to build relations between your models. As mentioned by Jared in this link for the foreign key to exist, you need to specify a belongs_to constraint.
The distinction is in where you place the foreign key (it goes on the table for the class declaring the belongs_to association)
class Product < ApplicationRecord
belongs_to :cart_content
end
If you also want to access all Products for User you can go for below, but you probably don't need a through relation.
class User < ApplicationRecord
has_many :cart_contents
has_many :products, through: :cart_contents
end
Then your Product model would look like this
class Product < ApplicationRecord
belongs_to :cart_content
belongs_to :user
end
I found a solution that works for me.
product.rb
class Product < ApplicationRecord
has_one :cart_content
end
cart_content.rb
class CartContent < ApplicationRecord
belongs_to :user
belongs_to :product
end
user.rb
class User < ApplicationRecord
has_many :cart_contents
has_many :products, through: :cart_contents # for convenience
end
Rails no longer expects a cart_content_id column on Product and the following commands all work in the console:
user = User.first
product = Product.first
cc = CartContent.new
cc.user = user
cc.product = product
cc.save
User.first.products # returns products currently in cart

Extracting data using rails query from a join table

I have users table, books table and books_users join table. In the users_controller.rb I am trying extract the users who have filtered_books. Please help me to resolve that problem.
user.rb
has_many :books_users, dependent: :destroy
has_and_belongs_to_many :books, join_table: :books_users
book.rb
has_and_belongs_to_many :users
books_user.rb
belongs_to :user
belongs_to :book
users_controller.rb
def filter_users
#filtered_books = Fiction.find(params[:ID]).books
#users = **I want only those users who have filtered_books**
end
has_and_belongs_to_many does not actually use a join model. What you are looking for is has_many through:
class User < ApplicationRecord
has_many :book_users
has_many :books, through: :book_users
end
class Book < ApplicationRecord
has_many :book_users
has_many :users, through: :book_users
end
class BookUser < ApplicationRecord
belongs_to :book
belongs_to :user
end
If you want to add categories to books you would do it by adding a Category model and another join table. Not by creating a Fiction model which will just create a crazy amount of code duplication if you want multiple categories.
class Book < ApplicationRecord
has_many :book_users
has_many :users, through: :book_users
has_many :book_categories
has_many :categories, through: :book_categories
end
class BookCategory < ApplicationRecord
belongs_to :book
belongs_to :category
end
class Category < ApplicationRecord
has_many :book_categories
has_many :books, through: :book_categories
end
If you want to query for users that follow a certain book you can do it by using an inner join with a condition on books:
User.joins(:books)
.where(books: { title: 'Lord Of The Rings' })
If you want to get books that have a certain category:
Book.joins(:categories)
.where(categories: { name: 'Fiction' })
Then for the grand finale - to query users with a relation to at least one book that's categorized with "Fiction" you would do:
User.joins(books: :categories)
.where(categories: { name: 'Fiction' })
# or if you have an id
User.joins(books: :categories)
.where(categories: { id: params[:category_id] })
You can also add an indirect association that lets you go straight from categories to users:
class Category < ApplicationRecord
# ...
has_many :users, though: :books
end
category = Category.includes(:users)
.find(params[:id])
users = category.users
See:
The has_many :through Association
Joining nested assocations.
Specifying Conditions on Joined Tables
From looking at the code i am assuming that Book model has fiction_id as well because of the has_many association shown in this line Fiction.find(params[:ID]).books. There could be two approaches achieve this. First one could be that you use #filtered_books variable and extract users from it like #filtered_books.collect {|b| b.users}.flatten to extract all the users. Second approach could be through associations using fiction_id which could be something like User.joins(:books).where(books: {id: #filtererd_books.pluck(:id)})

Rails has_many :through scoped with joins

I'm trying to create a has_many :through association with a custom scope. The problem is that the scope has a joins with a 3rd model, and that seems to be breaking the association.
These are the models:
class Student < ApplicationRecord
has_many :lesson_applications, through: :lesson_requests
has_many :lessons, through: :lesson_applications
has_many :scoped_lessons, -> { custom_scope }, through: :lesson_applications, source: :lesson
has_many :tutors, through: :scoped_lessons # Broken association
end
class Lesson < ApplicationRecord
belongs_to :lesson_application
has_one :tutor, through: :lesson_application
has_one :time_range, as: :time_rangeable
scope :custom_scope, (lambda {
joins(:time_range).where('time_ranges.attribute BETWEEN ? AND ?', value1, value2)
})
end
class LessonApplication < ApplicationRecord
belongs_to :lesson_request
belongs_to :tutor
has_one :lesson
end
class Tutor < ApplicationRecord
has_many :lesson_applications
has_many :lessons, through: lesson_applications
end
class TimeRange < ApplicationRecord
belongs_to :time_rangeable, polymorphic: true
end
I expected to get the student's tutors from the scoped lessons, but instead I get an missing FROM-clause entry for table 'time_ranges'. Which is clearly and error that can be seen on the generated query.
The generated query when I do student.tutors is:
SELECT "tutors".* FROM "tutors" INNER JOIN "lesson_applications" ON "tutors"."id" = "lesson_applications"."tutor_id" INNER JOIN "lessons" ON "lesson_applications"."id" = "lessons"."lesson_application_id" INNER JOIN "lesson_applications" "lesson_applications_tutors" ON "lessons"."lesson_application_id" = "lesson_applications_tutors"."id" INNER JOIN "lesson_requests" ON "lesson_applications_tutors"."lesson_request_id" = "lesson_requests"."id" WHERE "lesson_requests"."student_id" = $1 AND (time_ranges.start BETWEEN '2018-04-11 20:27:34.798992' AND '2018-04-11 20:27:34.799090') LIMIT $2
The method I'm using temporarily instead of the association is:
class Student < ApplicationRecord
def tutors
scoped_lessons.map(&:tutor).uniq
end
end
I am using ruby 2.5.1 with Rails 5.1.6
Thank you in advance.
Edit: some associations were wrong.

How do I specify the join table on a has_many through association?

Here is the schema information on my tables:
table_name: admin_users, primary_key: id
table_name: UserCompanies, primary_key: UserCompanyId, foreign_keys: [CompanyId, UserId]
table_name: Companies, primary_key: CompanyId'
I want to do something like the following:
AdminUser.first.companies
But, my attempts so far are not working, I'm assuming because I need to specify the table names, model names, or key names, but I don't know how that works with a has_many through relationship. Here is my best attempt at defining it so far:
class AdminUser < ActiveRecord::Base
has_many :user_companies, class_name:"TableModule::UserCompany", foreign_key:"UserId"
has_many :companies, through: :user_companies, class_name: "TableModule::Company"
end
# this code is from a rails engine separate from the app where AdminUser is defined
# the purpose of the engine is to provide access to this particular database
# the CustomDBConventions class adapts the models for this database to work with ActiveRecord so we can use snake case attributes, reference the primary key as 'id', and it specifies the correct tables names.
module TableModule
class UserCompany < CustomDBConventions
belongs_to :admin_user
belongs_to :company
end
class Company < CustomDBConventions
has_many :admin_users, through: :user_companies
end
class CustomDBConventions < ActiveRecord::Base
self.abstract_class = true
def self.inherited(subclass)
super
subclass.establish_connection "table_module_#{Rails.env}".to_sym
tb_name = subclass.table_name.to_s.gsub(/^table_module_/,"").classify.pluralize
subclass.table_name = tb_name
subclass.primary_key = tb_name.singularize + "Id"
subclass.alias_attribute :id, subclass.primary_key.to_sym
subclass.column_names.each do |pascal_name|
subclass.alias_attribute pascal_name.underscore.to_sym, pascal_name.to_sym
subclass.alias_attribute "#{pascal_name.underscore}=".to_sym, "#{pascal_name}=".to_sym
end
end
end
end
EDIT: So this setup is really close and I am missing only 1 foreign key specification. When I run AdminUser.first.companies I get a sql error:
TinyTds::Error: Invalid column name 'company_id'.: EXEC sp_executesql N'SELECT [Companies].* FROM [Companies] INNER JOIN [UserCompanies] ON [Companies].[CompanyId] = [UserCompanies].[company_id] WHERE [UserCompanies].[UserId] = #0', N'#0 int', #0 = 1
So I just need to specify to use UserCompanies.CompanyId on this join. How do I properly specify this foreign key?
Assuming the TableModule::UserCompany model has these associations...
class TableModule::UserCompany < ActiveRecord::Base
belongs_to :admin_user
belongs_to :company
end
...then I think this is what you're after:
class AdminUser < ActiveRecord::Base
has_many :companies, through: :user_company, class_name: "TableModule::UserCompany"
end
I'm uncertain what you're doing with the TableModule prefixes, but the following should work:
class AdminUser < ActiveRecord::Base
has_many :user_companies
has_many :companies, through: :user_companies
end
class Company < ActiveRecord::Base
has_many :user_companies
has_many :admin_users, through: :user_companies
end
class UserCompany < ActiveRecord::Base
belongs_to :admin_user
belongs_to :comany
end

How can I multiple insert the data in rails

I want to insert multiple data in rails. I'm using postgresql, the scenario is when the form submit it passes client name, email and some personal info, then also pass the desire venue with the date and also the amenities they want (ex. swimming poll, billiard poll and etc.). In my backend I will query :
venue = Venue.find(theVenue_id)
book = venue.books.new(name: client_name, email: client_email and etc)
My question is how can I insert the data in my amenity_books if the had many amenities choosen?
I trie something like this.
ex. amenities_id_choosen = [1,3]
if book.save
amenities_id_choosen.each do |x|
amenity = Amenitiy.find(x)
amenity_book = amenity.amenity_books.create(venue_id: venue.id)
end
I know this is not a good idea to insert data but that was my last choice. Does any body knows how to insert multiple data in 2 model with different data.
Models
class Amenity < ActiveRecord::Base
has_many :categorizations
has_many :venues, through: :categorizations
has_many :amenity_books
has_many :books, through: :amenity_books
end
class Venue < ActiveRecord::Base
has_many :categorizations
has_many :amenities, through: :categorizations
end
class Categorization < ActiveRecord::Base
belongs_to :venue
belongs_to :amenity
end
class Book < ActiveRecord::Base
belongs_to :venue
end
class AmenityBook < ActiveRecord::Base
belongs_to :amenity
belongs_to :venue
belongs_to :book
end
Here's an improved version:
amenities_id_choosen = [1,3]
if book.save
Amenitiy.find(amenities_id_choosen).each do |amenity|
amenity.amenity_books.create(venue_id: venue.id)
end
end
This will result in one SELECT query to find all chosen amenities and one INSERT query for each selected amenity.
Another option is to change your data model, does AmenityBook really need to have a venue? Because it looks like the venue is defined through the Book model already.
Here's a suggestion:
class Book < ActiveRecord::Base
belongs_to :venue
has_many :amenity_books
has_many :amenities, through: :amenity_books
end
class AmenityBook < ActiveRecord::Base
belongs_to :amenity
belongs_to :book
has_one :venue, through: :book
end
The code to create a booking with many amenities:
amenities_id_choosen = [1,3]
book.amenity_ids = amenities_id_choosen
if book.save
# success !
end

Resources