I am developing a Product model that has an has_one association to itself including a raw material record. And the model returns data through REST API. I am using Active Model Serializer instead of JBuilder.
Product model has a 'code' field that contain a product code in string:
'001-000-01-01' (This is a product.)
'001-000-00-01' (This is a material.)
Only difference between two codes is the third number from right. '1' is product. '0' is material. I want to include a "raw_material" record when retrieving a product record. So, I try to set has_one association with scope that has a "where" clause (later I can compose a query to get a material from a product code). Now I simply pass "product" object in lambda and use it in where.
First I write in def raw_material, this works. However, I don't know how to pass an object to def and use it in where clause. Therefore I come up with the scope pattern in has_one, however it returns an error even though it generates exactly the same SELECT as the def pattern. I get "NoMethodError" instead.
class Product < ActiveRecord::Base
has_many :supplies, ->{order('row_order ASC') }, primary_key: :code, foreign_key: :product_code
#This Works!
#has_one :raw_material, class_name: 'Product', primary_key: :code, foreign_key: :code
#This Works!
#has_one :raw_material
#Does not work. Why?
has_one :raw_material, ->(product) { where('code = ?', product.code).take }, class_name: 'Product'
accepts_nested_attributes_for :supplies, allow_destroy: true
#def raw_material
# Product.where('code = ?', '001-000-01-01').take
#end
end
The "def" pattern works:
Started GET "/products/1.json" for ::1 at 2016-10-05 21:48:15 +0900
Processing by ProductsController#show as JSON
Parameters: {"id"=>"1"}
Product Load (0.3ms) SELECT "products".* FROM "products" WHERE "products"."id" = ? LIMIT 1 [["id", 1]]
[active_model_serializers] Supply Load (0.1ms) SELECT "supplies".* FROM "supplies" WHERE "supplies"."product_code" = ? ORDER BY row_order ASC [["product_code", "031-052-00-01"]]
[active_model_serializers] Product Load (0.1ms) SELECT "products".* FROM "products" WHERE (code = '001-000-01-01') LIMIT 1
[active_model_serializers] Rendered ProductSerializer with ActiveModelSerializers::Adapter::Attributes (6.71ms)
Completed 200 OK in 24ms (Views: 9.2ms | ActiveRecord: 1.4ms)
However, the scope pattern does not work:
Started GET "/products/1.json" for ::1 at 2016-10-06 08:19:13 +0900
Processing by ProductsController#show as JSON
Parameters: {"id"=>"1"}
Product Load (0.1ms) SELECT "products".* FROM "products" WHERE "products"."id" = ? LIMIT 1 [["id", 1]]
[active_model_serializers] Supply Load (0.1ms) SELECT "supplies".* FROM "supplies" WHERE "supplies"."product_code" = ? ORDER BY row_order ASC [["product_code", "031-052-00-01"]]
[active_model_serializers] Product Load (0.1ms) SELECT "products".* FROM "products" WHERE (code = '031-052-00-01') LIMIT 1
[active_model_serializers] Rendered ProductSerializer with ActiveModelSerializers::Adapter::Attributes (10.6ms)
Completed 500 Internal Server Error in 27ms (ActiveRecord: 1.2ms)
NoMethodError (undefined method `except' for #<Product:0x007fe97b090418>):
app/controllers/products_controller.rb:17:in `block (2 levels) in show'
app/controllers/products_controller.rb:14:in `show'
Rendered vendor/bundle/ruby/2.2.0/gems/actionpack-4.2.5/lib/action_dispatch/middleware/templates/rescues/_source.erb (8.8ms)
Product Controller simply defines show like this:
# GET /products/1
# GET /products/1.json
def show
respond_to do |format|
format.html
format.json do
render json: #product, include: ['raw_material']
end
end
end
product_serializer.rb is:
class ProductSerializer < ActiveModel::Serializer
attributes(*Product.attribute_names.map(&:to_sym))
has_one :raw_material
end
Any assistance is greatly appreciated.
Thank you
UPDATE:
I solved this issue myself. Please check my answer below. Thank all who wrote solutions.
You can customise the has_one :raw_material as follows.
class ProductSerializer < ActiveModel::Serializer
attributes(*Product.attribute_names.map(&:to_sym))
has_one :raw_material do
Product.where(code: object.code)
end
end
I could solve this myself. Since I don't have to write this in a line, the def(method) pattern is the best bet. How to override "has_many" could help my issue.
Overriding a has_many association getter
"self" can be used for has_one to get an object inside def.
has_one :raw_material
def raw_material
Product.where('code like ?', (self.code)[0,9] + '_' + (self.code)[10,3]).take
end
This works and can generate any dataset you like in a Model.
# Does not work. Why?
has_one :raw_material ->(product) { where('code = ?', product.code).take }...
because you are trying to define a scope here... not an association. if you want a scope, use a scope eg:
scope :raw_material ->(product) { where('code = ?', product.code).first }
if you want an association, then use an association eg
has_one :raw_material, class_name: 'Product', primary_key: :code, foreign_key: :code
don't try to mix the two.
Related
I have a Project model, that can have itself as sub-projects defined as Project.projects.
class Project < ApplicationRecord
belongs_to :parent_project, class_name: 'Project', optional: true
has_many :projects, foreign_key: :parent_project_id, class_name: 'Project', dependent: :destroy
has_many :goals
end
Each Project has_many Goals.
I am trying to write a method in the Project model that will enable me to gather all of the goals of this project and it's children projects (recursively) for all sub-projects.
def descendant_projects
self.projects | self.projects.map(&:descendant_projects).flatten
end
def goals_including_descendants
all_goals = self.goals
descendant_projects.each do |project|
all_goals.concat(project.goals)
end
all_goals
end
When I call project.goals_including_descendants, the project_id for the children's projects are getting updated in the database to be direct goals of the original parent project. What part of this code would be causing rails to trigger a database update? I can see it in my console as:
Goal Load (0.0ms) SELECT "goals".* FROM "goals" WHERE "goals"."project_id" = $1 [["project_id", 49]]
(0.0ms) BEGIN
SQL (0.2ms) UPDATE "goals" SET "project_id" = $1, "updated_at" = $2 WHERE "goals"."id" = $3 [["project_id", 1], ["updated_at", "2020-05-14 20:47:47.761861"], ["id", 19]]
I am totally stumped as to why this is happening. Thanks for any insight.
It happens because concat adds the items to the self.goals AR Relation object and updates it immediately.
You could get around this by casting it to an array.
all_goals = self.goals.to_a
descendant_projects.each do |project|
all_goals << project.goals.to_a
end
all_goals.flatten
which will return an array in the end, which may or may not be what you're looking for.
Another way would be to get all the ids. This will return a Relation/Enumerable in the end.
all_goals_ids = self.goals.ids
descendant_projects.each do |project|
all_goals_ids << project.goals.ids
end
Goal.where(id: all_goals_ids)
I'm doing meal ordering system in Rails, I made model for User, Restaurant, Meal and Order and I don't know which associations should I use to connect all these models together. For now my models looks like this:
class User < ApplicationRecord
has_many :orders
has_many :restaurants, through: :orders
end
class Order < ApplicationRecord
belongs_to :user
belongs_to :restaurant
end
class Meal < ApplicationRecord
belongs_to :restaurant
end
class Restaurant < ApplicationRecord
has_many :orders
has_many :meals
has_many :users, through: :orders
end
Now when I'm using form to order some meal and save this order in the database I'm getting error in log:
Parameters: {"utf8"=>"✓", "authenticity_token"=>"+pwoJ/82k/2SiS7z4X4nVHyaKCMMfWCECQe6TufnkpNaW9PEgvlwxlf3skAH2QQupSLIoe81Z/I0CleL/m9cjw==", "orders"=>{"restaurant_id"=>"2", "meal_id"=>"2", "suggestions"=>""}, "commit"=>"Place your order"}
Unpermitted parameters: restaurant_id, meal_id
(0.1ms) begin transaction
(0.1ms) rollback transaction
Redirected to http://localhost:3000/
Completed 302 Found in 8ms (ActiveRecord: 0.2ms)
My order controller looks like this:
class OrdersController < ApplicationController
def new
#order = Order.new
end
def create
#order = Order.new(order_params)
if #order.save
redirect_to root_path
flash[:success] = "Your order has been added"
else
flash[:danger] = "Error"
redirect_to root_path
end
end
private
def order_params
params.require(:orders).permit(:restaurant, :meal, :suggestions)
end
end
When I change def order_params to:
params.require(:orders).permit(:restaurant_id, :meal_id, :suggestions)
I'm getting
unknown attribute 'restaurant_id' for Order.
I assume that it's bad associations fault, can anyone help me?
* UPDATE *
Now I'm getting error in my log when I'm trying to save order:
Started POST "/new_order" for 127.0.0.1 at 2016-12-19 11:18:50 +0100
Processing by OrdersController#create as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"nnDqY0FVzUslTJ1VoL57vnlO6aSTLcVuenT1GJwloJ8+txGAPJoucOAyAeZGGVjEoPYJJnBlwhhHeRjdha1ugw==", "orders"=>{"restaurant_id"=>"4", "meal_id"=>"26", "suggestions"=>""}, "commit"=>"Place your order"}
(0.1ms) begin transaction
Restaurant Load (0.1ms) SELECT "restaurants".* FROM "restaurants" WHERE "restaurants"."id" = ? LIMIT ? [["id", 4], ["LIMIT", 1]]
(0.1ms) rollback transaction
Redirected to http://localhost:3000/
Completed 302 Found in 6ms (ActiveRecord: 0.3ms)
* Update 2 *
When I change for #order.save! I get:
Validation failed: User must exist
With order.errors.inspect I just get:
(0.2ms) rollback transaction
No template found for OrdersController#create, rendering head :no_content
Completed 204 No Content in 97ms (ActiveRecord: 1.8ms)
I'm using omniauth to sign in before user can order a meal and this is only way to sign up or sign in. When I created Order model I used user:references, you think this can be a reason?
You said that you have a sign_in/sign_up, so I assume that you have a current_user in your controller.
In order to save your order you need to supply user_id, here is a one way to do it
def order_params
params.require(:orders).permit(:restaurant, :meal, :suggestions)
.merge(user_id: current_user.id)
end
I have models for User, Profile and Organisation Request. The associations are:
User
has_one :profile, dependent: :destroy
has_one :organisation_request, through: :profile
accepts_nested_attributes_for :organisation_request
Profile
belongs_to :user
belongs_to :organisation
Organisation Request
belongs_to :profile
# belongs_to :user#, through: :profile
belongs_to :organisation
In my user model, I have a method called full_name (which I use to format the presentation of a user's name.
I'm trying to access that full_name method in my organisation_requests model.
I'm trying to do that by writing the following method in my organisation requests model:
def related_user_name
self.profile.user.full_name
end
When I try to use this in my organisation requests index, like this:
<%= link_to orgReq.related_user_name, organisation_request.profile_path(organisation_request.profile.id) %>
I get an error that says:
undefined method `user' for nil:NilClass
When I try to use this idea in the rails console, with:
o = OrganisationRequest.last
OrganisationRequest Load (0.4ms) SELECT "organisation_requests".* FROM "organisation_requests" ORDER BY "organisation_requests"."id" DESC LIMIT 1
=> #<OrganisationRequest id: 2, profile_id: 1, organisation_id: 1, created_at: "2016-08-01 22:48:52", updated_at: "2016-08-01 22:48:52">
2.3.0p0 :016 > o.profile.user.formal_name
Profile Load (0.5ms) SELECT "profiles".* FROM "profiles" WHERE "profiles"."id" = $1 LIMIT 1 [["id", 1]]
User Load (0.5ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT 1 [["id", 1]]
=> " John Test"
The concept seems to work in the console?
Can anyone see where I've gone wrong?
Don't chain methods, it's a bad practice, it violates the Law of Demeter. The best choice is to use the delegate. So instead of:
def related_user_name
self.profile.user.full_name
end
You can have:
class OrganisationRequest
belongs_to :profile
has_one :user, through: :profile
delegate :full_name, to: :user, allow_nil: true, prefix: true
end
Then you can just call organisation_request.user_full_name and it will go through profile > user and call full_name (and you won't get undefined since the allow_nil: true will "cover" it)
More info about delegate here.
have you checked all of your organisation requests have profile? may be this is not best practice, try to use profile.try(:user).try(:full_name)
I have the following models, User, Dad, Mom, Follow and Kid.
class User
has_many :kids
has_many :follows
geocoded_by :address
end
class Dad
has_many :kids
has_many :follows
end
class Mom
has_many :kids
geocoded_by :address
end
class Follow
belongs_to :dad
end
class Kid
belongs_to :mom
belongs_to :dad
belongs_to :user
end
With the Geocoder gem I'm trying to create a scope that takes in the total count of the Dad's Kids nearest to the User's location compared to the Kids Mom's locations that happened today.
<% #follows.each do |f| %>
<% f.dad.kids.today.nearest.count
<% end %>
In my ApplicationController:
def set_user_location
if signed_in?
#user_location = current_user.address
end
end
In my Kid model:
scope :today, -> { where("kids.created_at > ?", Time.now.beginning_of_day) }
scope :nearest, -> { joins(:mom).merge(Mom.near(#user_location, 2000)) }
But the nearest doesn't work. It doesn't load Moms at all.
User Load (0.6ms) SELECT "users".* FROM "users" WHERE "users"."id" = 5 ORDER BY "users"."id" ASC LIMIT 1
Processing by FollowsController#index as HTML
Follow Exists (0.3ms) SELECT 1 AS one FROM "follows" WHERE "follows"."user_id" = $1 LIMIT 1 [["user_id", 5]]
Follow Load (0.5ms) SELECT "follows".* FROM "follows" INNER JOIN "dads" ON "dads"."id" = "follows"."dad_id" WHERE "follows"."user_id" = $1 ORDER BY name ASC LIMIT 30 OFFSET 0 [["user_id", 5]]
Product Load (0.2ms) SELECT "dads".* FROM "dads" WHERE "dads"."id" = $1 ORDER BY "dads"."id" ASC LIMIT 1 [["id", 39]]
(0.7ms) SELECT COUNT(*) FROM "kids" WHERE "kids"."dad_id" = $1 AND (kids.created_at > '2014-05-08 04:00:00.000000') [["dad_id", 39]]
CACHE (0.0ms) SELECT COUNT(*) FROM "kids" WHERE "kids"."dad_id" = $1 AND (kids.created_at > '2014-05-08 04:00:00.000000') [["dad_id", 39]]
What would be the correct way to write the nearest scope?
Problem is with today scope, not nearest.
Every model has sreated_at field so in your scope you should write
scope :today, -> { where("kids.created_at > ?", Time.now.beginning_of_day) }
EDIT:
Another error is that you're referring to controller instance variable in model. Model and controller are separated so it will not work this way. Instead you should rewrite scope to
scope :nearest, ->(location, distance=2000) { joins(:mom).merge(Mom.near(location, distance)) }
and then in the controller you may write
Kid.nearest(some location goes here)
but I think your models are a little messed up and you want them to do something different.
WhatI suppose you want to do is find a mother that is nearest to kid, so rather you should do something like
Mom.near(some_kid, 2000)
I have following models
class User < ActiveRecord::Base
has_many :project_users, dependent: :destroy
has_many :projects, through: :project_users
end
class ProjectUser < ActiveRecord::Base
belongs_to :user
belongs_to :project
has_many :participants
has_many :tasks, through: :participants
end
class Task < ActiveRecord::Base
belongs_to :project
has_many :participants
has_many :project_users, through: :participants
accepts_nested_attributes_for :participants
end
class Participant < ActiveRecord::Base
belongs_to :project_user
belongs_to :task
end
So the flow should go like this:
1. User creates the project
2. User adds users to project via the join model ProjectUser
3. User creates a task for the project and selects those users from ProjectUsers, who will participate in the task. So I put an accepts_nested_attributes_for method and tried to build the nested form.
In my controller:
def new
#task = Task.new
#task.participants.build
end
def task_params
params.require(:task).permit(:project_id, :project_phase_id, :priority_id, :title, :due_date, :estimation, :responsible_id, :description, :participant_ids => [])#, :participants_attributes => [:project_user_id, :task_id])
end
participants_attributes is commented
In my view:
= f.association :participants, as: :select
Actual HTML generated:
<input name="task[participant_ids][]" type="hidden" value="">
<select class="select optional form-control" id="task_participant_ids" multiple="multiple" name="task[participant_ids][]">
<option value="57">AlexandeR MazeiN</option>
<option value="59">Firenze</option>
<option value="58">Vasily Strekotkin</option>
</select>
I add options via ajax, value = ProjectUser.id
I Have to do it like so, because I dont know which ProjectUsers there will be unless a concrete project for a task is selected.
Error I am getting:
Started POST "/tasks" for 127.0.0.1 at 2014-04-11 13:18:24 +0300
User Load (0.7ms) SELECT "users".* FROM "users" WHERE "users"."id" = 6 ORDER BY "users"."id" ASC LIMIT 1
Processing by TasksController#create as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"aXuk9ZuDvFZce+sbIQLRhWZlVjitMvySaJ7CwWfdmaQ=", "task"=>{"project_id"=>"20", "priority_id"=>"4", "project_phase_id"=>"40", "title"=>"Skepta", "due_date"=>"", "estimation"=>"8", "responsible_id"=>"6", "participant_ids"=>["", "57", "58"], "description"=>""}, "commit"=>"Create Task"}
Team Load (0.4ms) SELECT "teams".* FROM "teams" WHERE "teams"."id" = $1 LIMIT 1 [["id", 3]]
Participant Load (0.5ms) SELECT "participants".* FROM "participants" WHERE "participants"."id" IN (57, 58)
Completed 404 Not Found in 7ms
ActiveRecord::RecordNotFound - Couldn't find all Participants with IDs (57, 58) (found 0 results, but was looking for 2):
Your param hash has the IDs from ProjectUser as participant_ids, so when it queries the database it is looking for Participant models with these IDs. You need to set these as project_user_id inside of a list of participants, something like this:
participants: [ { project_user_id: 57 }, { project_user_id: 58 } ]
I'm not super familiar with build, but something along these lines should allow AR to properly construct the associations.