Querying a join table? - ruby-on-rails

I am trying to do two things:
query an attribute from an inner join table in Rails' Console.
query and displaying the attribute in a view.
These are my Models:
retreat.rb:
class Retreat < ApplicationRecord
belongs_to :user
belongs_to :account
validates :name, presence: true
has_many :retreats_teams
has_many :teams, through: :retreats_teams
accepts_nested_attributes_for :retreats_teams
end
retreats_team.rb:
class RetreatsTeam < ApplicationRecord
belongs_to :team
belongs_to :retreat
end
team.rb:
class Team < ApplicationRecord
belongs_to :account
has_many :team_members
has_many :users, through: :team_members
accepts_nested_attributes_for :team_members
has_many :retreats
has_many :retreats, through: :retreats_teams
end
In Rails' console, if I type:
Retreat.last.teams
I get the output:
irb(main):008:0> Retreat.last.teams
Retreat Load (0.9ms) SELECT "retreats".* FROM "retreats" ORDER BY "retreats"."id" DESC LIMIT $1 [["LIMIT", 1]]
Team Load (0.9ms) SELECT "teams".* FROM "teams" INNER JOIN "retreats_teams" ON "teams"."id" = "retreats_teams"."team_id" WHERE "retreats_teams"."retreat_id" = $1 LIMIT $2 [["retreat_id", 38], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy [#<Team id: 56, name: "My house", account_id: 2, created_at: "2020-02-10 15:57:25", updated_at: "2020-02-10 15:57:25">]>
irb(main):009:0>
How do I retrieve the team name: "My house"?
Also, there might be many teams that display here, too.

#teams returns a collection of team objects. The simplest solution is to call first on the teams to get the first team in the collection:
Retreat.last.teams.first.name
=> "My house"
But if you want all the names in teams you might use pluck. This will allow you to do this:
retreat = Retreat.last
foo = Team.create(name: 'Foo')
bar = Team.create(name: 'Bar')
retreat.teams << foo
retreat.teams << bar
retreat.teams.pluck(:name).to_sentence
=> "My house, Foo, and Bar"

A word on naming
The naming convention for join models is SingularSingular. The table should be named singular_plural. has_and_belongs_to_many is the only part of Rails that actually uses the oddball plural_plural naming scheme.
RetreatsTeam # bad
RetreatTeam # better
Even better though is to actually give your join tables meaningful names instead of just placeholder names.
1) querying an attribute from an inner join table in Rails Console.
Since the association between Retreat and RetreatsTeams in one to many you can actually only fetch aggregates. Otherwise which attribute should it fetch, from the first row, the last row or all the rows?
So for example you can do:
Retreat.joins(:retreats_teams)
.select('retreats.*', 'COUNT(retreats_teams.*) AS retreats_teams_count')
If you are storing more data on the join table that you want to display you want to iterate through the join table:
#retreat = Retreat.eager_load(retreats_teams: :teams).first
#retreat.retreats_teams.each do |rt|
puts rt.foo
puts rt.team.name
end
2) querying and displaying the attribute in a view.
In Rails you're usually just fetching records in the controller and then iterating through them in the view:
class ResortsController < ApplicationController
def show
#resort = Resort.includes(:teams).find(params[:id])
end
end
# app/views/resorts/show.html.erb
<h1><%= #resort.name %></h1>
<h2>Teams</h2>
<% if #resort.teams.any? %>
<ul>
<% #resort.teams.each do |team| %>
<li><%= team.name %></li>
<% end %>
</ul>
<% else %>
<p>This resort has no teams</p>
<% end %>

Related

many-to-many with multiple select in Rails

I have a model called Article, another model called Organization, and a join table called ArticleAllowedOrganization that has an article_id and organization_id in it.
When a user creates an article I would like for them to specify which organizations should have access to the article.
When I test in the console it seems to be working correctly, however, when I create an article in the web app the params for the through association allowed_organization_ids is empty.
Article model:
class Article < ActiveRecord::Base
has_many :article_allowed_organizations, dependent: :destroy
has_many :allowed_organizations, through: :article_allowed_organizations, source: :organization
end
Organization model:
class Organization < ActiveRecord::Base
has_many :article_allowed_organizations, dependent: :destroy
has_many :allowed_articles, through: :article_allowed_organizations, source: :article
end
the join table:
class ArticleAllowedOrganization < ActiveRecord::Base
belongs_to :article
belongs_to :organization
end
in the articles_controller.rb I permitted the array for allowed_organization_ids
def article_params
params.require(:article).permit(:other_stuff, :allowed_organization_ids => [])
end
In the form:
<%= f.collection_select(:allowed_organization_ids, Organization.order(:name), :id, :name,
{include_blank: true}, {multiple: true}) %>
In the console I can manually set multiple organization id's to be associated with an article and it works.
a = Article.last
a.allowed_organization_ids = [2, 4]
you can see below that it inserts the ids for an organization into the ArticleAlllowedOrganization table when I use the console.
(0.3ms) BEGIN
SQL (1.3ms) INSERT INTO "article_allowed_organizations" ("article_id", "organization_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id" [["article_id", 95], ["organization_id", 4], ["created_at", "2018-12-12 17:47:25.978487"], ["updated_at", "2018-12-12 17:47:25.978487"]]
(6.5ms) COMMIT
=> [2, 4]
when using the web app I get this in the log where the allowed_organization_ids isn't passing anything.
Parameters: {"....some_other_params" "allowed_organization_ids"=>[""]}
if I try and use collection_check_boxes
<%= f.collection_check_boxes :allowed_organization_ids, Organization.all, :id, :name do |b| %>
<div class="collection-check-box">
<%= b.check_box %>
<%= b.label %>
</div>
<% end %>
I get this in the log when using collection_check_boxes where it says true instead of the organization ids
"allowed_organization_ids"=>["true"]
also what's interesting is that I also get the below in th elog when trying to use checkboxes where it's trying to find an organization that has an id of 0 but can't which prevents the form from submitting.
[1m[35mOrganization Load (0.5ms)[0m SELECT "organizations".* FROM "organizations" WHERE "organizations"."id" = $1 LIMIT 1 [["id", 0]]
Completed 404 Not Found in 67ms (ActiveRecord: 2.4ms)
ActiveRecord::RecordNotFound (Couldn't find Organization with 'id'=0):
Please check follow are few issues. In your controller, you put
def article_params
params.require(:article).permit(:other_stuff, :allowed_organization_ids => [])
end
So you are already saying your allowed_organization_ids is empty, please replace with following
def article_params
params.require(:article).permit(:other_stuff, :allowed_organization_id)
end
I think you are just changing the name, so you have provided the different name of classes but here in the relationship you are mentioning different, but as your console is giving correct relationship this should not be an issue, but in case I am mentioning it.
class ArticleAllowedOrganization < ActiveRecord::Base
belongs_to :article
belongs_to :organization
end
should be allowed_article and allowed_organization
I am practicing so I created this in the project to see if it is working with proper names, so following are commands I used in the generator to create this whole project. You can try to see if there is any other error. Note I put permission field in the allowed table, all commands are on command line inside your project first command will create new project.
rails new SampleProject && cd $_ && bundle
rails g model Article title:string description:text
rails g model Organization name:string
rails g model Allowed permission:string article:references organization:references
Now make sure your Article model has following.
class Article < ActiveRecord::Base
has_many :alloweds
has_many :organizations, through: :alloweds
end
And in your Organization model
class Organization < ActiveRecord::Base
has_many :alloweds
has_many :articles, through: :alloweds
end
And in your allowed model, although I will use permission than allowed table name but it is your naming.
class Allowed < ActiveRecord::Base
belongs_to :article
belongs_to :organization
end
Now let us generate views and controllers
rails g scaffold_controller Article title:string description:string
rails g scaffold_controller Organization name:string
rails g scaffold_controller Article title:string description:string
Now put routes in your config/routes.rb
resources :articles
resources :organizations
resources :alloweds
Now you can start your rail server and navigate to /articles , /organization and /alloweds to see it working. Rails auto generator uses text field for relationship ids, so you can put manually ids and it will work.
Hope it is clear.

Rails not ordering by date for this query

I'm currently working on a website where a parent can reserve classes for their kids. In the parent's dashboard, I'd like to show a table of schedules and in every row there will be the child's name associated with a schedule. Unfortunately I have problem ordering the schedules by date in the parent's dashboard.
# view
<% #reservations.each do |reservation| %>
<%= reservation.child.first_name %>
<%= reservation.schedule.date %>
<%= reservation.schedule.start_time %> - <%= reservation.schedule.end_time %>
<%= link_to reservation.schedule.klass.name, schedule_path(reservation.schedule) %></td>
<%= link_to reservation.schedule.partner.company, partner_path(reservation.schedule.partner) %></td>
<%= reservation.schedule.city.name %></td>
<% end %>
# associations
User
has_many :children, dependent: :destroy
has_many :reservations
has_many :schedules, through: :reservations
Child
belongs_to :user
has_many :reservations, dependent: :destroy
has_many :schedules, through: :reservations
Reservation
belongs_to :child
belongs_to :schedule
belongs_to :user
Schedule
belongs_to :city
belongs_to :partner
belongs_to :activity
belongs_to :klass
has_many :reservations
has_many :children, through: :reservations
I've got a default scope in my Schedule model that orders by date, start_time and end_time.
# Schedule model
default_scope { order(:date, :start_time, :end_time) }
This scope works in other tables, but not for this query:
# controller
#reservations = current_user.reservations.includes(:child, schedule: [:partner, :klass, :city])
It just refuses to order by date and time in the browser:
The log shows that the query for Schedule is indeed being ordered:
Reservation Load (0.3ms) SELECT "reservations".* FROM "reservations" WHERE "reservations"."user_id" = $1 [["user_id", 1]]
Child Load (0.4ms) SELECT "children".* FROM "children" WHERE "children"."id" IN (1, 2, 3)
Schedule Load (0.4ms) SELECT "schedules".* FROM "schedules" WHERE "schedules"."id" IN (24, 12) ORDER BY "schedules"."date" ASC, "schedules"."start_time" ASC, "schedules"."end_time" ASC
Partner Load (0.3ms) SELECT "partners".* FROM "partners" WHERE "partners"."id" IN (2)
Klass Load (0.3ms) SELECT "klasses".* FROM "klasses" WHERE "klasses"."id" IN (9, 17)
City Load (0.4ms) SELECT "cities".* FROM "cities" WHERE "cities"."id" IN (28)
I could do this query in the controller instead:
#schedules = current_user.schedules
but then I'd have problem showing only a single child's name for each schedule since a class schedule can have many children associated for it.
Help?
You've defined an ordering on the default scope for Schedule, but you are pulling your relation from Reservation. As a result, the schedules are queried in the order specified, but since presumably you're looping through the #reservations in your view, you see their order and not their schedules's order. You can order #reservations by fields on the schedules table like so:
#reservations = #reservations.order("schedules.date ASC, schedules.start_time ASC, schedules.end_time ASC")
Also note that defining a default_scope in the model is generally discouraged (on SO and elsewhere) because its difficult to manage and can cause unexpected and unintuitive effects, especially down the road.

Rails + ActiveRecord - fighting with "N+1" issue

I have these models:
car:
class Car < ActiveRecord::Base
has_many :car_services, dependent: :destroy
has_many :services, through: :car_services
end
car_service
class CarService < ActiveRecord::Base
belongs_to :car
belongs_to :service
validates_uniqueness_of :service_id, scope: :car_id
end
truck
class Truck < ActiveRecord::Base
has_many :truck_services, dependent: :destroy
has_many :services, through: :truck_services
end
truck_service
class TruckService < ActiveRecord::Base
belongs_to :truck
belongs_to :service
end
service
class Service < ActiveRecord::Base
has_many :car_services
has_many :cars, through: :car_services
has_many :truck_services
has_many :trucks, through: :truck_services
end
In controllers' actions (there are separated actions for cars and for trucks) I query the models like this:
#cars = Car.find_by_sql("SELECT ...")
...
#trucks = Truck.find_by_sql("SELECT ...")
and then in the respective views:
<% #cars.each do |result| %>
<li data-services="<% result.services.each do |service| %><%= service.name %>,<% end %>">...</li>
...
<% end %>
This procedure works very well for cars, it's very fast.
Different action for trucks, but the same procedure:
<% #trucks.each do |result| %>
<li data-services="<% result.services.each do |service| %><%= service.name %>,<% end %>">...</li>
...
<% end %>
Now, this procedure is very slow. When I tried to debug why (because as I mentioned, the procedure is same for both models, but for cars it's working very fast even though that in the car model is 600k records and in the truck "only" 300k), I spotted in the console log following information:
...
Service Load (159.1ms) SELECT `services`.* FROM `services` INNER JOIN `truck_services` ON `services`.`id` = `truck_services`.`service_id` WHERE `truck_services`.`truck_id` = 35769
Service Load (166.8ms) SELECT `services`.* FROM `services` INNER JOIN `truck_services` ON `services`.`id` = `truck_services`.`service_id` WHERE `truck_services`.`truck_id` = 45681
Service Load (151.4ms) SELECT `services`.* FROM `services` INNER JOIN `truck_services` ON `services`.`id` = `truck_services`.`service_id` WHERE `truck_services`.`truck_id` = 50974
...
How is this possible? Why I don't see these lines also for cars and I see them for truck? Do I miss something? I was thinking about missing indexes in the MySQL tables, but there aren't used any.
I am fighting with this issue the whole Saturday, but I have no solution for this...
I would be very grateful for an advice how to fix this issue.
Thank you very much guys.
I will start with an advice: post your sql queries for others to understand exactly what you are doing and to be able to help you
To eliminate N+1 you should use includes or preload
In your controllers do this:
#cars = Car.includes(:services).all
respectively,
#trucks = Truck.includes(:services).all
You can see that I preferred a much cleaner syntax to find_by_sql.
When you run:
#cars = Car.includes(:services).all
this will trigger 3 queries:
SELECT "cars".* FROM "cars";
SELECT "car_services".* FROM "car_services" WHERE "car_services".car_id IN (?, ?, ? ...);
SELECT "services".* FROM "services" WHERE "services".car_service_id IN (?, ?, ? ...);
If you remove N+1 your page will load faster but you should check that all your keys have indexes defined, too.
The above its just an example. You should really post your complete queries for us to understand exactly what you are doing. And those models Car and Truck, they look very similar, maybe it will be a better solution to use Single Table Inheritance.
You could rewrite this:
<% #trucks.each do |result| %>
<li data-services="<% result.services.each do |service| %><%= service.name %>,<% end %>">...</li>
<% end %>
as this
<% #trucks.each do |truck| %>
<%= content_tag :li, '...', data: {services: truck.services.map(&:name).join(', ')} %>
<% end %>
Hope this will help you somehow.

Rails ActiveRecord has_many through association query

When I query directly against my database I get the expected result, but not in rails. I'm guessing it has to do with my associations and something bad I said about Ruby 3 years ago.
Postgres SQL Query:
SELECT users.email, members.software, members.files
FROM users INNER JOIN members
ON members.user_id = users.id
WHERE members.region_id=2
Result: "dan#gmail.com";t;t "dan#test.com";t;t
BUT from rails c:
> ←[1m←[36mUser Load (1.0ms)←[0m ←[1mSELECT users.email,
> members.software, members.files FROM "users" INNER JOIN "members" ON
> "members"."user_id" = "users"."id" WHERE "members"."region_id" =
> 2←[0m> => #<ActiveRecord::Relation [#<User id: nil, email: "dan#gmail.com">, #<User id: nil, email: "dan#test.com">]>
That snippet was the resulting query from pasting in what I have tried to create in my controller and hard coding the region id:
User.joins(:members).select("users.email, members.software, members.files").where(members: {region_id: params[:id]})
These are my models:
class User < ActiveRecord::Base
has_many :members
has_many :regions, :through => :members
end
class Region < ActiveRecord::Base
has_many :members
has_many :users, :through => :members
end
class Member < ActiveRecord::Base
belongs_to :user
belongs_to :region
end
Is it the way I have associated my models or something else that I am missing?
Thanks!
What you are getting is active_relations object.
you can access the attributes like this
users = User.joins(:members).select("users.email, members.software as software, members.files as files").where(members: {region_id: params[:id]})
users.each do |u|
p u.email
p u.software
p u.files
end

Rails 3 - dependent => :delete_all doesn't work. Why?

I have few models in my project. There are some of them: Qualification and Curriculum. Qualification has children (curriculums). I want to make sure that when i delete Qualification i delete all it's children. Here is me code:
# Table name: qualifications
#
# id :integer not null, primary key
# subject_id :integer
# teacher_id :integer
# created_at :datetime not null
# updated_at :datetime not null
class Qualification < ActiveRecord::Base
belongs_to :subject
belongs_to :teacher
has_many :curriculums, :dependent => :delete_all
has_many :school_classes, :through => :curriculums
end
# id :integer not null, primary key
# school_class_id :integer
# qualification_id :integer
# created_at :datetime not null
# updated_at :datetime not null
class Curriculum < ActiveRecord::Base
belongs_to :school_class
belongs_to :qualification
has_one :result
has_many :timetables
end
As you can see i tried to use :dependent => :delete_all in Qualification model. But it doesn't work. Why?
UPD:
I deleting qualification by unchecking checkboxes in view when i EDIT it:
<div class="control-group">
<%= f.label :subject_ids, "Teacher can teach such subjects in the school",
:class => "control-label" %>
<div class="controls">
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>Choose</th>
<th>Subject</th>
</tr>
</thead>
<tbody>
<%= hidden_field_tag "teacher[subject_ids][]", nil %> <%# We use hidden field because it doesn't submit unchecked fields. So, we pass nil and nothing will be submitted.%>
<% #subjects.each do |subject| %>
<tr>
<td>
<%= check_box_tag "teacher[subject_ids][]", # [] brackets tells that this is array.
subject.id, # Value of checkbox.
#teacher.subject_ids.include?(subject.id), # Here we automatically check checkboxes.
id: dom_id( subject ) %> <%# Give unique id for each value. 'dom_id' is Rails helper. We will have ids like: 'subject_1', 'subject_2' and etc. %>
</td>
<td>
<%= label_tag dom_id( subject ), subject.subject_name %> <%# Put name of subject. %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
Here is more info:
class Teacher < ActiveRecord::Base
has_many :qualifications
has_many :subjects, :through => :qualifications
end
class Subject < ActiveRecord::Base
has_many :qualifications
has_many :teachers, :through => :qualifications
end
Here is SQL code when i update my model:
Started PUT "/teachers/2" for 127.0.0.1 at 2012-06-03 18:34:44 +0400
Processing by TeachersController#update as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"ZJNNV9/TO6k18O1Ar1kpkU+PWbd7btHm9Tc067iMNO4=", "teacher"=>{"teacher_last_name"=>"Last", "teacher_first_name"=>"First", "teacher_middle_name"=>"Middle", "teacher_sex"=>"m", "teacher_birthday"=>"1980-12-01", "teacher_phone_attributes"=>{"teacher_mobile_number"=>"88283686", "teacher_home_number"=>"5112787", "id"=>"2"}, "teacher_education_attributes"=>{"teacher_education_university"=>"Mmm", "teacher_education_year"=>"1970-01-01", "teacher_education_graduation"=>"Graduated", "teacher_education_speciality"=>"Math", "id"=>"2"}, "teacher_category"=>"1st", "subject_ids"=>["", "4", "3", "1"]}, "commit"=>"Update", "id"=>"2"}
User Load (0.5ms) SELECT "users".* FROM "users" WHERE "users"."id" = 2 LIMIT 1
Teacher Load (0.4ms) SELECT "teachers".* FROM "teachers" WHERE "teachers"."id" = $1 LIMIT 1 [["id", "2"]]
(0.1ms) BEGIN
Subject Load (0.5ms) SELECT "subjects".* FROM "subjects" WHERE "subjects"."id" IN (4, 3, 1)
Subject Load (0.5ms) SELECT "subjects".* FROM "subjects" INNER JOIN "qualifications" ON "subjects"."id" = "qualifications"."subject_id" WHERE "qualifications"."teacher_id" = 2
SQL (0.4ms) DELETE FROM "qualifications" WHERE "qualifications"."teacher_id" = 2 AND "qualifications"."subject_id" = 2
TeacherPhone Load (0.4ms) SELECT "teacher_phones".* FROM "teacher_phones" WHERE "teacher_phones"."teacher_id" = 2 LIMIT 1
TeacherEducation Load (0.4ms) SELECT "teacher_educations".* FROM "teacher_educations" WHERE "teacher_educations"."teacher_id" = 2 LIMIT 1
(24.1ms) COMMIT
Redirected to http://0.0.0.0:3000/teachers
Completed 302 Found in 57ms (ActiveRecord: 27.2ms)
Try:
:dependent => :destroy
instead of :dependent => :delete_all
Not sure if this is what you are doing but I'm able to delete dependents using :dependent => :delete_all
Instead of using delete, I used destroy in my controller.
Example:
Instead of:
Qualification.delete(params[:id])
Use this:
Qualification.destroy(params[:id])
Hope this helps. =)
the "the rails 3 way" says that -
(pg 187)
clear
Transactionally removes all records from this association by clearing the foreign key
field (see delete). If the association is configured with the :dependent option set to
:delete_all, then it calls delete_all. Similarly, if the :dependent option is set to
:destroy_all, then the destroy_all method is invoked.
delete(*records) and delete—all
The delete and delete_all methods are used to sever specified associations, or all of
them, respectively. Both methods operate transactionally.
It’s worth noting, for performance reasons, that calling delete_all first loads the
entire collection of associated objects into memory in order to grab their ids. Then it
executes a SQL UPDATE that sets foreign keys for all currently associated objects to nil,
effectively disassociating them from their parent. Since it loads the entire association into memory, it would be ill-advised to use this method with an extremely large collection of associated objects.
Note
The names of the delete and delete_all methods can be misleading. By default, they
don’t delete anything from the database—they only sever associations by clearing the foreign
key field of the associated record. This behavior is related to the :dependent option, which
defaults to :nullify. If the association is configured with the :dependent option set to
:delete or :destroy, then the associated records will actually be deleted from the database.
:dependent => :destroy or :delete
Specifies a rule that the associated owner record should be destroyed or just deleted from
the database, depending on the value of the option. When triggered, :destroy will call
the dependent’s callbacks, whereas :delete will not.
Usage of this option might make sense in a has_one / belongs_to pairing. However,
it is really unlikely that you want this behavior on has_many / belongs_to relationship;
it just doesn’t seem to make sense to code things that way. Additionally, if the owner
record has its :dependent option set on the corresponding has_many association, then
destroying one associated record will have the ripple effect of destroying all of its siblings.

Resources