Optimizing database query in Rails 5 - ruby-on-rails

I have 4 models
module Vehicle
has_many :routes
end
module Route
has_many :route_users
belongs_to :vehicle
end
module RouteUser
belongs_to :route
belongs_to :user
end
module User
belongs_to :route_user
end
My goal is to return the most recent driver (a user) through the aggregate.rb; to be specific, I need user id, first_name, last_name.
attributes = {
id: vehicle.id,
... other attributes ...
}
attributes.merge!(
driver: {
id: vehicle.routes.last.route_users.last.user.id,
first_name: vehicle.routes.last.route_users.last.user.first_name,
last_name: vehicle.routes.last.route_users.last.user.last_name
}
) if vehicle.routes.present? && vehicle.routes.last.route_users.present?
As you can see, .merge! loads a bunch of information and dramatically slows down the aggregate.rb return. Is there any way to optimize this return to make it faster? Am I missing something?

You can improve your User model to make the query easier.
class User < ApplicationRecord
has_many :route_users
has_many :routes, through: :route_users
has_many :vehicles, through: :routes
end
And when the query is big from one side, the best way is two invert the logic, and make a query from user, example:
First fetch last_user_drive, and after that, use his fields to merge into attributes
last_user_driver = User.joins(routes: :vehicle).where(vehicle: {id: vehicle.id}).order('routes.created_at').last
...
attributes.merge!(
driver: {
id: last_user_driver.id,
first_name: last_user_driver.first_name,
last_name: last_user_driver.last_name
}
) if last_user_driver.present?

Related

How to structure a has_many association with a dynamic scope?

I have a users table in my db. A user can be either of type 'admin' or 'manager'.
Given the models and schema below, I would like that for each instance of 'manager' user, an 'admin' user could select one, some or all the locations of the tenant that the manager belongs to in order to select which locations the manager can have control over.
My models
class User < ActiveRecord::Base
belongs_to :tenant
class Tenant < ActiveRecord::Base
has_many :users, dependent: :destroy
has_many :locations, dependent: :destroy
class Location < ActiveRecord::Base
belongs_to :tenant, inverse_of: :locations
I've tried two paths
First, trying to establish a scoped has_many association between the User and the Location models. However, I can't wrap my head around structuring this scope so that an 'admin' user could select which locations the 'manager' users can control.
Second, setting up a controlled_locations attribute in the users table. Then I set up some code so that an 'admin' user can select which locations a 'manager' can control, populating its 'controlled_locations' attribute. However, what gets saved in the database (inside the controlled_locations array) is strings instead of instances of locations.
Here's the code that I tried for the second path:
The migration
def change
add_column :users, :controlled_locations, :string, array: true, default: []
end
In the view
= f.input :controlled_locations, label: 'Select', collection: #tenant_locations, include_blank: "Anything", wrapper_html: { class: 'form-group' }, as: :check_boxes, include_hidden: false, input_html: {multiple: true}
In the users controller (inside the update method)
if params["user"]["controlled_locations"]
params["user"]["controlled_locations"].each do |l|
resource.controlled_locations << Location.find(l.to_i)
end
resource.save!
end
What I expect
First of all, I'm not quite sure the second path that I tried is a good approach (storing arrays in the db). So my best choice would be to set up a scoped association if it's possible.
In case the second path is feasible, what I would like to get is something like this. Let's say that logging in an Admin, I selected that the user with ID 1 (a manager) can control one location (Boston Stadium):
user = User.find(1)
user.controlled_locations = [#<Location id: 55, name: "Boston Stadium", created_at: "2018-10-03 12:45:58", updated_at: "2018-10-03 12:45:58", tenant_id: 5>]
Instead, what I get after trying is this:
user = User.find(1)
user.controlled_locations = ["#<Location:0x007fd2be0717a8>"]
Instead of instances of locations, what gets saved in the array is just plain strings.
First, your code is missing the locations association in the Tenant class.
class Tenant < ActiveRecord::Base
has_many :users, dependent: :destroy
has_many :locations
Let's say the variable manager has a User record. Then the locations it can control are:
manager.tenant.locations
If you want, you can shorten this with a delegate statement.
class User < ActiveRecord::Base
belongs_to :tenant
delegate :locations, to: :tenant
then you can call this with
manager.locations
A common pattern used for authorization is roles:
class User < ApplicationRecord
has_many :user_roles
has_many :roles, through: :user_roles
def add_role(name, location)
self.roles << Role.find_or_create_by(name: name, location: location)
end
def has_role?(name, location)
self.roles.exists?(name: name, location: location)
end
end
# rails g model role name:string
# make sure you add a unique index on name and location
class Role < ApplicationRecord
belongs_to :location
has_many :user_roles
has_many :users, through: :user_roles
validates_uniqueness_of :name, scope: :location_id
end
# rails g model user_role user:references role:references
# make sure you add a unique compound index on role_id and user_id
class UserRole < ApplicationRecord
belongs_to :role
belongs_to :user
validates_uniqueness_of :user_id, scope: :role_id
end
class Location < ApplicationRecord
has_many :roles
has_many :users, through: :roles
end
By making the system a bit more generic than say a controlled_locations association you can re-use it for different cases.
Let's say that logging in an Admin, I selected that the user with ID 1
(a manager) can control one location (Boston Stadium)
User.find(1)
.add_role(:manager, Location.find_by(name: "Boston Stadium"))
In actual MVC terms you can do this by setting up roles as a nested resource that can be CRUD'ed just like any other resource. Editing multiple roles in a single form can be done with accepts_nested_attributes or AJAX.
If you want to scope a query by the presence of a role then join the roles and user roles table:
Location.joins(roles: :user_roles)
.where(roles: { name: :manager })
.where(user_roles: { user_id: 1 })
To authenticate a single resource you would do:
class ApplicationController < ActionController::Base
protected
def deny_access
redirect_to "your/sign_in/path", error: 'You are not authorized.'
end
end
class LocationsController < ApplicationController
# ...
def update
#location = Location.find(params[:location_id])
deny_access and return unless current_user.has_role?(:manger, #location)
# ...
end
end
Instead of rolling your own authorization system though I would consider using rolify and pundit.

Loading Relations

Goal: I would like to include all of a customers medical conditions as an array in the result of a customer.
for:
cust = Customer.includes(:conditions).find(1)
expected result:
#<Customer id: 1, first_name: "John", last_name: "Doe", conditions [...]>
actual result:
#<Customer id: 1, first_name: "John", last_name: "Doe">
code:
I have 2 classes and a 3rd join class (ConditionsCustomer).
class Customer < ApplicationRecord
has_many :conditions_customers
has_many :conditions, through: :conditions_customers
end
#join table. Contains 2 foreign_keys (customer_id, condition_id)
class ConditionsCustomer < ApplicationRecord
belongs_to :customer
belongs_to :condition
end
class Condition < ApplicationRecord
has_many :conditions_customers
has_many :customers, through: :conditions_customers
end
What's interesting is that I see 3 select queries getting fired (customer, join table and medical conditions table) so I know the includes is somewhat working but unfortunately customer returns without the medical conditions.
I've also tried using a join but I get an array of same customer over and over again.
Is there an easy way to do this with ActiveRecord? I would prefer not having to merge the record manually.
Not really possible via active record, as json offers some cool possibilities :
render json: customers,
include: {
conditions: {
only: [:attr1, :attr2], # filter returned fields
methods: [:meth1, :meth2] # if you need model methods
},
another_joined_model: {
except: [:password] # to exclude specific fields
}
}

Rails: why is calling `valid?` only validate some of associated record not all associated records

I have a model Order which is like
# app/models/order.rb
class Order< ApplicationRecord
has_one :detail
has_one :extra
..
end
I have two orders
order1 = Order.first
order1.detail #<OrderDetail:0x00 name: "abc", remark: 'test1'>
order1.extra #<OrderExtra:0x00 email: nil, recipent: nil>
order2 = Order.second
order1.detail #<OrderDetail:0x00 name: "abc", remark: 'test1'>
order1.extra #<OrderExtra:0x00 email: nil, recipent: "xyz">
When I call order1.valid? or order1.save! it will not check OrderExtra validation and returns true. But when I call order2.valid? or order2.save! it checks OrderExtra validation.
order1.save! # true
order2.save! # ActiveRecord Invalid OrderExtra
I want to know how rails checks if they want to check associated validation when call save! and the reason behind that.
Please let me know if any additional requirement needed on this.
use the validates_associated for enforcing associated model validations
class Book < ActiveRecord::Base
has_many :pages
belongs_to :library
validates_associated :pages, :library
end
This validation will not fail if the association hasn’t been assigned. If you want to ensure that the association is both present and guaranteed to be valid, you also need to use validates_presence_of.
class Library < ActiveRecord::Base
has_many :books
validates_presence_of :name
end

Update attributes on has_many through associations and working with the unsaved object

This has something to do with my last quesion about unsaved objects, but now it is more about a specific problem how to use rails.
The models I have are:
class User < ActiveRecord::Base
has_many :project_participations
has_many :projects, through: :project_participations, inverse_of: :users
end
class ProjectParticipation < ActiveRecord::Base
belongs_to :user
belongs_to :project
enum role: { member: 0, manager: 1 }
end
class Project < ActiveRecord::Base
has_many :project_participations
has_many :users, through: :project_participations, inverse_of: :projects
accepts_nested_attributes_for :project_participations
end
With this models, when I create a new project I can do it by a form (fields_for etc) and then I can call update_attributes in the controller. So if I have users in the database already, I can do this:
u = Users.create # save one user in database (so we have at least one saved user)
p = Project.new
# add the user to the project as a manager
# the attributes could come from a form with `.fields_for :project_participations`
p.update_attributes(project_participations_attributes: [{user_id: u.id, role: 1}])
=> true
This works fine until I want to do something with the users of a project. For example I want add a validations that there must be at least one user for a project:
class Project < ActiveRecord::Base
...
validates :users, presence: true # there must be at least one user in a project
...
end
This now gives:
u = Users.create
p = Project.new
p.update_attributes(project_participations_attributes: [{user_id: u.id, role: 1}])
=> false
p.errors
=> #<ActiveModel::Errors:... #base=#<Project id: nil>, #messages={:users=>["can't be blank"]}>
p.users
=> #<ActiveRecord::Associations::CollectionProxy []>
p.project_participations
=> #<ActiveRecord::Associations::CollectionProxy [#<ProjectParticipation id: nil, user_id: 1, project_id: nil>]>
So on unsaved projects the .users is empty. This already bugs me (see my last quesion about unsaved objects). But in this case I can of course now work around this by doing validates :project_participations, presence: true instead of validates :users, presence: true and it should mean the same.
But this would mean I should never use the .users method (in any helper, model, view, ...) unless I am totally sure that I work with a saved object. Which in fact renders the .users method unusable (like it does with the validation of user`s presence).
If I call update_attributes like this, the validations works and it saves:
p.update_attributes(users: [u])
With this it creates the project_participation by itself so p.users works as expected. But here I cannot set any data like role for project_participation of that user.
So my questions are: Can I make the .users method work whether or not the object is saved (I think not)? But then, how can I add users to a unsaved project as a manager/member and work with the unsaved project?
I hope my problem is clear.
I think I understand you question, and you're correct in assuming that you cannot use the .users method whether or not the project model is saved. The reason for this is that in defining an association in Project (ie. has_many :users, through: :project_participations, inverse_of: :projects) you're telling rails to read the users attribute out of the database via the project_participations join table and when you haven't saved the project you have nothing to read out of the database.
In order to add a User to your project in a particular role you will need to create a new ProjectParticipation model which you will then associate to your project. If you then remove the users association and write your own users method you should be able to access your collection of users regardless of whether or not the project has been saved.
class Project < ActiveRecord::Base
has_many :project_participations
...
def users
project_participations.collect { |pp| pp.user }
end
end
Then something like:
u = Users.create
p = Project.new
pp = ProjectParticipation.new({user: u, project: p, role: 1})
p.project_participations << pp
p.users
Hopefully that helps.

How can I populate a join (many-to-many) table inside seeds.rb for ruby on rails?

I currently have a model for team.rb and user.rb, which is a many to many relationship. I have created the join table teams_users but I am not sure how to populate this table in my seeds.rb?
For example, I have :
user = User.create({ first_name: 'Kamil', last_name: 'Bo', email: 'bo#gmail.com'})
team = Team.create([{ name: 'Spot Forwards', num_of_games: 10, day_of_play: 4}])
But the following does not work???
TeamsUsers.create({ team_id: team.id, user_id: user.id })
I get a message :
uninitialized constant TeamsUsers
This isn't optimized but
user.team_ids = user.team_ids < team.id
user.save
or if this is the first team
user.team_ids = [team.id]
user.save
ALso start using has_many :through. then you will have a TeamUser model. it's a life saver if the join table needs more attributes
Pick a side to work from and then, as #drhenner suggests, use the _ids property to create the association. For example, working with the User model, create the teams first, then the users, assigning them to teams as you go:
teams = Team.create([
{ name: 'Team 1' },
{ name: 'Team 2' },
{ name: 'Team 3' },
# etc.
])
User.create([
{ name: 'User 1', team_ids: [teams[0].id, teams[2].id] },
{ name: 'User 2', team_ids: [teams[1].id, teams[2].id] },
{ name: 'User 3', team_ids: [teams[0].id, teams[1].id] },
# etc.
])
From comment above:
You can have multiple relationships configured on a has_many :through relationship. It's up to you which ones you want to implement. These are all the possibilities:
class Team < ApplicationRecord
has_many :memberships
has_many :users, through: :memberships
end
class Membership < ApplicationRecord
belongs_to :team
belongs_to :user
end
class User < ApplicationRecord
has_many :memberships
has_many :teams, through: :memberships
end
So, when dealing with the Team model, you can use: team.memberships and team.users;
when dealing with the User model, you can use: user.memberships and user.teams;
and if dealing with the join model, you can use: membership.team and membership.user.
You can omit the relationship references to the join model if you don't use it—especially if you're treating the relationship between Team and User like a standard has_and_belongs_to_many relationship:
class Team < ApplicationRecord
has_many :users, through: :memberships
end
class User < ApplicationRecord
has_many :teams, through: :memberships
end
This gives you team.users and user.teams.
Using Rails, say you have tables foos and bars in a many-to-many relationship using table foos_bars, you can seed their associations like this:
bar1 = Bar.find(1)
bar2 = Bar.find(2)
foo1 = Foo.find(1) # for example
foo1.bars << bar1
foo1.bars << bar2
foo1.save
This will update the joins table foos_bars with associations <foo_id:1, bar_id:1>, <foo_id:1, bar_id:2>
Hope this helps.

Resources