I am trying to create an account management system that allows an account to have one billing address. I want the account and address to have their own controller and model. An admin user would create a new account then be redirected to create a new billing address for this account. The first part works, I am able to create the account, but the second only saves the account_id attribute to the addresses table. Below is my code:
accounts_controller
class AccountsController < ApplicationController
def new
#account = Account.new
end
def create
#account = Account.new(params[:account])
#account.build_address
if #account.save
flash[:success] = "Customer Account has been successfully created!"
redirect_to '/newaddress'
else
render 'new'
end
end
end
addresses_controller
class AddressesController < ApplicationController
def new
#address = Address.new
end
def create
#account = Account.find(params[:account])
#address = #account.create_address(params[:address])
redirect_to root_path
end
end
account_model
class Account < ActiveRecord::Base
has_one :address
accepts_nested_attributes_for :address
attr_accessible :contactFirstName, :contactLastName, :contactEmail, :contactPhone, :business_name
end
address_model
class Address < ActiveRecord::Base
attr_accessible :city, :state, :street, :zipCode
belongs_to :account
end
I also added the following to my routes.rb file
resources :accounts do
resources :addresses
end
Last, below is the schema for the relavant tables
create_table "accounts", :force => true do |t|
t.string "contactEmail"
t.string "contactFirstName"
t.string "contactLastName"
t.string "contactPhone"
t.datetime "joinDate"
t.string "business_name"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "addresses", :force => true do |t|
t.string "city"
t.string "state"
t.string "street"
t.string "zipCode"
t.integer "account_id"
t.datetime "created_at"
t.datetime "updated_at"
end
(I am still new to programming and rails so I posted as much as I thought would be relevant.)
Added Log from when I open the new account page and then proceed to the new address page.
Started GET "/newaccount" for 127.0.0.1 at Tue Sep 20 17:21:56 -0500 2011
Processing by AccountsController#new as HTML
Rendered shared/_error_messages.html.erb (0.6ms)
Rendered accounts/_account_fields.html.erb (13.0ms)
Rendered layouts/_stylesheets.html.erb (1.2ms)
User Load (2.5ms) SELECT `users`.* FROM `users` WHERE `users`.`id` IS NULL LIMIT 1
CACHE (0.0ms) SELECT `users`.* FROM `users` WHERE `users`.`id` IS NULL LIMIT 1
Rendered layouts/_header.html.erb (19.4ms)
Rendered layouts/_footer.html.erb (1.0ms)
Rendered accounts/new.html.erb within layouts/application (50.3ms)
Completed 200 OK in 168ms (Views: 64.6ms | ActiveRecord: 2.5ms)
Started POST "/accounts" for 127.0.0.1 at Tue Sep 20 17:22:11 -0500 2011
Processing by AccountsController#create as HTML
Parameters: {"commit"=>"Next", "account"=>{"business_name"=>"FooBar", "contactLastName"=>"Bar", "contactPhone"=>"1231231234", "contactEmail"=>"foo#bar.com", "contactFirstName"=>"Foo"}, "authenticity_token"=>"lJG89TIjcJighmFUWLg1uR9sJq0CHLvceeLH9QNocGY=", "utf8"=>"✓"}
SQL (0.1ms) BEGIN
SQL (0.3ms) SELECT 1 FROM `accounts` WHERE (LOWER(`accounts`.`contactEmail`) = LOWER('foo#bar.com')) LIMIT 1
SQL (1.2ms) describe `accounts`
AREL (0.4ms) INSERT INTO `accounts` (`created_at`, `contactFirstName`, `business_name`, `contactPhone`, `updated_at`, `contactEmail`, `contactLastName`, `joinDate`) VALUES ('2011-09-20 22:22:11', 'Foo', 'FooBar', '1231231234', '2011-09-20 22:22:11', 'foo#bar.com', 'Bar', NULL)
SQL (1.5ms) describe `addresses`
AREL (0.2ms) INSERT INTO `addresses` (`zipCode`, `state`, `city`, `updated_at`, `account_id`, `street`, `created_at`) VALUES (NULL, NULL, NULL, '2011-09-20 22:22:11', 31, NULL, '2011-09-20 22:22:11')
SQL (26.3ms) COMMIT
Redirected to http://localhost:3500/newaddress
Completed 302 Found in 85ms
Started GET "/newaddress" for 127.0.0.1 at Tue Sep 20 17:22:11 -0500 2011
Processing by AddressesController#new as HTML
Rendered addresses/_address_fields.html.erb (83.4ms)
Rendered layouts/_stylesheets.html.erb (1.4ms)
User Load (0.2ms) SELECT `users`.* FROM `users` WHERE `users`.`id` IS NULL LIMIT 1
CACHE (0.0ms) SELECT `users`.* FROM `users` WHERE `users`.`id` IS NULL LIMIT 1
Rendered layouts/_header.html.erb (17.1ms)
Rendered layouts/_footer.html.erb (0.9ms)
Rendered addresses/new.html.erb within layouts/application (113.6ms)
Completed 200 OK in 126ms (Views: 119.6ms | ActiveRecord: 30.3ms)
Started POST "/newaddress" for 127.0.0.1 at Tue Sep 20 17:22:29 -0500 2011
Processing by AddressesController#new as HTML
Parameters: {"address"=>{"city"=>"Boston", "street"=>"123 Main St", "zipCode"=>"02222", "state"=>"MA"}, "commit"=>"Create", "authenticity_token"=>"lJG89TIjcJighmFUWLg1uR9sJq0CHLvceeLH9QNocGY=", "utf8"=>"✓"}
Rendered addresses/_address_fields.html.erb (10.3ms)
Rendered layouts/_stylesheets.html.erb (1.2ms)
User Load (0.2ms) SELECT `users`.* FROM `users` WHERE `users`.`id` IS NULL LIMIT 1
CACHE (0.0ms) SELECT `users`.* FROM `users` WHERE `users`.`id` IS NULL LIMIT 1
Rendered layouts/_header.html.erb (17.2ms)
Rendered layouts/_footer.html.erb (0.9ms)
Rendered addresses/new.html.erb within layouts/application (40.1ms)
Completed 200 OK in 54ms (Views: 47.3ms | ActiveRecord: 0.2ms)
The line in in the Address class looks wrong to me (take out the _attributes):
class Address < ActiveRecord::Base
attr_accessible :city, :state, :street, :zipCode
belongs_to :account
end
As quick look, you are passing only params[:account], tail log and see what you need
class AddressesController < ApplicationController
def new
#address = Address.new
end
def create
#account = Account.find(params[:account])
#address = #account.create_address(params[:address])
redirect_to root_path
end
end
Ass you added log, you can see now that when you post newaddress
Parameters: {"address"=>{"city"=>"Boston", "street"=>"123 Main St", "zipCode"=>"02222", "state"=>"MA"}, "commit"=>"Create", "authenticity_token"=>"lJG89TIjcJighmFUWLg1uR9sJq0CHLvceeLH9QNocGY=", "utf8"=>"✓"}
and your controller have
def create
#account = Account.find(params[:account])
#address = #account.create_address(params[:address])
redirect_to root_path
end
As you can see there are no #account = Account.find(params[:account])
So you account is null, you may add hidden field with account_id to form or catch other way depends of of your app.
Related
i am building an eCommerce app in rails which has two products Chairs and Bookcabinets, i have created two different controllers and models for chairs and bookcabinets, it all worked now i wanted the user to be able to add these products to a cart so i created a ordering system from scratch by watching a tutorial, it worked for chairs but when i add bookcabinets to shopping_cart model it gives me an error: Couldn't find Chair with 'id'=
Server Log
Started POST "/cart/items" for 127.0.0.1 at 2018-09-26 14:16:43 +0530
(0.7ms) SELECT "schema_migrations"."version" FROM "schema_migrations" ORDER BY "schema_migrations"."version" ASC
Processing by OrderItemsController#create as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"wGAl7gZGXipKt7EzzZT1LuBgn2k8KnlaPagp0cQ3l6pIkr6mx8MdJAuhgkY7EEttrHjTSSpcRjqe0qZ0a2hrAA==", "bookcabinet_id"=>"1", "quantity"=>"40"}
Chair Load (0.8ms) SELECT "chairs".* FROM "chairs" WHERE "chairs"."id" = ? LIMIT ? [["id", nil], ["LIMIT", 1]]
Completed 404 Not Found in 106ms (ActiveRecord: 3.8ms)
ActiveRecord::RecordNotFound (Couldn't find Chair with 'id'=):
app/models/shopping_cart.rb:20:in `add_item'
app/controllers/order_items_controller.rb:8:in `create'
My Order_items_controller
class OrderItemsController < ApplicationController
def index
#items = current_cart.order.items
end
def create
current_cart.add_item(
chair_id: params[:chair_id],
bookcabinet_id: params[:bookcabinet_id],
quantity: params[:quantity]
)
redirect_to cart_path
end
def destroy
current_cart.remove_item(id: params[:id])
redirect_to cart_path
end
end
My Order_controller
class OrdersController < ApplicationController
def new
#order = current_cart.order
end
end
** My Order model**
class Order < ApplicationRecord
has_many :items, class_name: 'OrderItem'
end
My Order_item Model
class OrderItem < ApplicationRecord
belongs_to :order
belongs_to :chair
belongs_to :bookcabinet
end
My Shopping_Cart Model
class ShoppingCart
delegate :sub_total, to: :order
def initialize(token:)
#token = token
end
def order
#order ||= Order.find_or_create_by(token: #token) do |order|
order.sub_total = 0
end
end
def items_count
order.items.sum(:quantity)
end
def add_item(chair_id:, bookcabinet_id:, quantity: 1)
chair = Chair.find(chair_id),
bookcabinet = Bookcabinet.find(bookcabinet_id)
order_item = order.items.find_or_initialize_by(
chair_id: chair_id,
bookcabinet_id: bookcabinet_id
)
order_item.price = chair.price
order_item.price = bookcabinet.price
order_item.quantity = quantity
ActiveRecord::Base.transaction do
order_item.save
update_sub_total!
end
end
def remove_item(id:)
ActiveRecord::Base.transaction do
order.items.destroy(id)
update_sub_total!
end
end
private
def update_sub_total!
order.sub_total = order.items.sum('quantity*price')
order.save
end
end
My Create_order migration
class CreateOrders < ActiveRecord::Migration[5.1]
def change
create_table :orders do |t|
t.string :first_name
t.string :last_name, null: false
t.decimal :sub_total, precision: 15, scale: 2, null: false
t.timestamps
end
end
end
my CreateOrderItems migration
class CreateOrderItems < ActiveRecord::Migration[5.1]
def change
create_table :order_items do |t|
t.belongs_to :order, null: false
t.belongs_to :chair, null: false
t.belongs_to :bookcabinet
t.integer :quantity, null: false
t.decimal :price, precision: 15, scale: 2, null: false
t.timestamps
end
end
end
Routes
get '/cart', to: 'order_items#index'
resources :order_items, path: '/cart/items'
get '/cart/checkout', to: 'orders#new', as: :checkout
patch '/cart/checkout', to: 'orders#create'
Add To Cart Form
<div class="button my-2">
<%= form_tag order_items_path do %>
<%= hidden_field_tag :bookcabinet_id, #bookcabinet.id %>
<%= number_field_tag :quantity, 1 %>
<button type="submit" class="btn cart"><i class="d-inline fa fa-cart-arrow-down" aria-hidden="true"></i><p class="d-inline">Add To Cart</p></button>
<% end %>
Updated Server Log
Started POST "/cart/items" for 127.0.0.1 at 2018-09-27 00:24:13 +0530
(0.6ms) SELECT "schema_migrations"."version" FROM "schema_migrations" ORDER BY "schema_migrations"."version" ASC
Processing by OrderItemsController#create as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"gqwsV8Z/0p74Y8WPLMmfk9vmoSPCFpP+lAtVxxH3KSvWUzYYIFtbQRkAzM5yh5HS/wAzelr90LJW64joFUpGwg==", "bookcabinet_id"=>"1", "quantity"=>"40"}
Bookcabinet Load (0.7ms) SELECT "bookcabinets".* FROM "bookcabinets" WHERE "bookcabinets"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
Order Load (0.9ms) SELECT "orders".* FROM "orders" WHERE "orders"."token" = ? LIMIT ? [["token", "20897ec5db2636f5"], ["LIMIT", 1]]
OrderItem Load (0.5ms) SELECT "order_items".* FROM "order_items" WHERE "order_items"."order_id" = ? AND "order_items"."chair_id" IS NULL AND "order_items"."bookcabinet_id" = ? LIMIT ? [["order_id", 4], ["bookcabinet_id", 1], ["LIMIT", 1]]
(0.2ms) begin transaction
Bookcabinet Load (0.3ms) SELECT "bookcabinets".* FROM "bookcabinets" WHERE "bookcabinets"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
(0.4ms) SELECT SUM(quantity*price) FROM "order_items" WHERE "order_items"."order_id" = ? [["order_id", 4]]
(0.2ms) commit transaction
Redirected to http://localhost:3000/cart
Completed 302 Found in 557ms (ActiveRecord: 10.4ms)
Started GET "/cart" for 127.0.0.1 at 2018-09-27 00:24:14 +0530
Processing by OrderItemsController#index as HTML
Order Load (0.5ms) SELECT "orders".* FROM "orders" WHERE "orders"."token" = ? LIMIT ? [["token", "20897ec5db2636f5"], ["LIMIT", 1]]
Rendering order_items/index.html.erb within layouts/application
(0.4ms) SELECT SUM("order_items"."quantity") FROM "order_items" WHERE "order_items"."order_id" = ? [["order_id", 4]]
OrderItem Load (1.1ms) SELECT "order_items".* FROM "order_items" WHERE "order_items"."order_id" = ? [["order_id", 4]]
Rendered order_items/index.html.erb within layouts/application (15.0ms)
CACHE (0.1ms) SELECT SUM("order_items"."quantity") FROM "order_items" WHERE "order_items"."order_id" = ? [["order_id", 4]]
Rendered layouts/_header.html.erb (22.0ms)
Rendered layouts/_footer.html.erb (1.9ms)
Completed 200 OK in 1313ms (Views: 1304.9ms | ActiveRecord: 2.1ms)
You are searching for chair item every time you call add_item method so its throwing an error as you are not passing chair id so you need to modify the method as below
def add_item(chair_id:, bookcabinet_id:, quantity: 1)
chair = Chair.find(chair_id) if chair_id
bookcabinet = Bookcabinet.find(bookcabinet_id) if bookcabinet_id
order_item = order.items.find_or_initialize_by(
chair_id: chair_id,
bookcabinet_id: bookcabinet_id
)
order_item.price = chair.price if chair
order_item.price = bookcabinet.price
order_item.quantity = quantity
ActiveRecord::Base.transaction do
order_item.save
update_sub_total!
end
end
Please feel free to ask if there are any questions regarding the same
please help solve the problem.
table posts
create_table "posts", force: :cascade do |t|
t.string "title"
t.text "body"
end
table tags
create_table "tags", force: :cascade do |t|
t.string "tagname"
end
joined table
create_table "posts_tags", id: false, force: :cascade do |t|
t.integer "post_id"
t.integer "tag_id"
end
model Post
class Post < ActiveRecord::Base
has_and_belongs_to_many :tags
end
model Tag
class Tag < ActiveRecord::Base
has_and_belongs_to_many :posts
end
I need to remove some of the association table posts_tags.
I get from the form set id tags:
[3, 18, 21]
Here is my controller that handles this set id_tags:
def update
if #post.update_attributes(post_params)
#add_new_tags(#post)
p '------------------1'
p params['delete_tags']
p '------------------2'
destroy_tags(params['delete_tags'], #post)
flash[:success] = t :post_updated
redirect_to user_post_path(#user, #post)
else
flash[:error] = t :post_not_updated
render 'edit'
end
end
private
def destroy_tags(tags,post)
tags.each do |tag|
p '=================='
p tag
tag_del = post.tags.find_by_post_id(:post_id => post.id)
if teg_del
post.tags.delete(tag_del)
end
end
end
def post_params
params.require(:post).permit(:delete_tags)
end
as a result, I get the following error message:
undefined method `find_by_post' for #<Tag::ActiveRecord_Associations_CollectionProxy:0x007f097ed78ac8>
the console displays the following message:
Processing by PostsController#update as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"jfNoFJVn2dDqIwIdK/SWerFKIPFJ9bTYdWjy4QFBFL8gHrO7TnzhBq4Mcw+uDyDn9atLEAmfcPdlmHBVHMSDHQ==", "post"=>{"title"=>"Corrupti.ggh", "body"=>"Suscipit ut odit labore fugiat quia aliquam."}, "tagnames"=>"", "delete_tags"=>["3", "18", "21"], "commit"=>"Сохранить Post", "locale"=>"ru", "user_id"=>"24", "id"=>"359"}
Post Load (0.2ms) SELECT "posts".* FROM "posts" WHERE "posts"."id" = ? LIMIT 1 [["id", 359]]
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1 [["id", 24]]
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."remember_token" = ? LIMIT 1 [["remember_token", "15166203712e74cc4638f34991c141f85c04a0e0"]]
CACHE (0.0ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1 [["id", "24"]]
(0.1ms) begin transaction
(0.0ms) commit transaction
"------------------1"
["3", "18", "21"]
"------------------2"
"=================="
"3"
Completed 500 Internal Server Error in 6ms (ActiveRecord: 0.5ms)
Since you have a join table, the tags table itself doesn't have a post_id, so that find_by won't work. It isn't needed though because post.tags would retrieve all the tags connected to that post anyway.
You get a lot of the functionality for handling habtm relationships and forms out of the box with rails using things like collection_check_boxes where I don't think you need the extra destroy tags method unless I'm misunderstanding what you are trying to accomplish. You might consider adding/removing tag associations to posts like this:
Post - has_and_belongs_to_many :tags
Tag - has_and_belongs_to_many :posts
Post Update Form:
<legend>Tags</legend>
<div class="form-group">
<%= f.collection_check_boxes(:tag_ids, Tag.all, :id, :name) %>
</div>
Make sure to allow :tag_ids => [] in your controller's post_params
The will allow you to update the tags belonging to the post by adding any new tags that were checked and removing any tags that were unchecked in the form and you can have normal create and update controller actions without need for the extra destroy_tags method.
Unable to update state attribute of the Product model using accepts_nested_attributes. This is what JSON I post looks like:
{"auth_token"=>"x", "product"=>{"caption"=>"x","state_attributes"=>{"id"=>"1"}}, "id"=>"x"}
All I need to do is to change referenced state. Thoughts?
State Model
class State < ActiveRecord::Base
has_many :products
end
Product Model
class Product < ActiveRecord::Base
attr_accessible :state, :state_attributes
belongs_to :state
accepts_nested_attributes_for :state, :allow_destroy => false
end
Products Controller Update Action
def update
product = Product.find_by_id( params[:id])
if product && product.update_attributes(params[:product])
respond_with(product, status: :updated, location: product)
else
respond_with(product.errors, status: :unprocessable_entity)
end
end
Transaction Log
Started PUT "/products/1170.json" for 127.0.0.1 at 2014-01-13 16:21:52 -0700
Processing by ProductsController#update as JSON
Parameters: {"auth_token"=>"x", "product"=>{"caption"=>"x", "state_attributes"=>{"id"=>"3"}}}, "id"=>"1170"}
WARNING: Can't verify CSRF token authenticity
User Load (0.7ms) SELECT "users".* FROM "users" WHERE "users"."authentication_token" = 'x' LIMIT 1
(0.1ms) BEGIN
(0.3ms) UPDATE "users" SET "last_sign_in_at" = '2014-01-13 23:20:46.614453', "current_sign_in_at" = '2014-01-13 23:21:52.251028', "sign_in_count" = 954, "updated_at" = '2014-01-13 23:21:52.251753' WHERE "users"."id" = 536
(6.5ms) COMMIT
Product Load (0.5ms) SELECT "products".* FROM "products" WHERE "products"."user_id" = 536 AND "products"."id" = 1170 LIMIT 1
(0.1ms) BEGIN
State Load (0.2ms) SELECT "states".* FROM "states" WHERE "states"."id" = 2 LIMIT 1
(0.5ms) ROLLBACK
Completed 404 Not Found in 15ms
ActiveRecord::RecordNotFound (Couldn't find State with ID=3 for Product with ID=1170):
app/controllers/products_controller.rb:81:in `update'
Rendered /Users/me/.rvm/gems/ruby-2.0.0-p353/gems/actionpack-3.2.11/lib/action_dispatch/middleware/templates/rescues/_trace.erb (1.0ms)
Rendered /Users/me/.rvm/gems/ruby-2.0.0-p353/gems/actionpack-3.2.11/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb (0.8ms)
Rendered /Users/me/.rvm/gems/ruby-2.0.0-p353/gems/actionpack-3.2.11/lib/action_dispatch/middleware/templates/rescues/diagnostics.erb within rescues/layout (5.8ms)
As #Sergio Aristizábal pointed out, I was trying to reset associated state instead of simply updating the id
You are trying to reset the id of the associated State. I guess you just want to associate a state to product, so you should just set the state_id attribute.
With that, I updated Product model
class Product < ActiveRecord::Base
attr_accessible :state_id, :state_attributes
belongs_to :state
end
Passed state id in the json
{"auth_token"=>"x", "product"=>{"caption"=>"x","state_id"=>"1"}, "id"=>"x"}
and everything worked like a charm. Thanks #Sergio
Trying to set up nested associations ( with accepts_nested_attributes_for ) in Rails 3, but save is failing without showing any errors on the model.
db/schema.rb:
create_table "question_responses", :force => true do |t|
t.string "answer"
t.boolean "correct"
t.integer "question_id"
t.integer "quiz_result_id"
end
create_table "quiz_results", :force => true do |t|
t.integer "student_id"
t.integer "quiz_id"
t.text "message"
end
question_response.rb:
class QuestionResponse < ActiveRecord::Base
belongs_to :question
belongs_to :quiz_result, :inverse_of => :question_responses
validates :question, :presence => true
validates :quiz_result, :presence => true
before_save :check_answer
end
quiz_result.rb:
class QuizResult < ActiveRecord::Base
validates :quiz, :presence => true
validates :student, :presence => true
validates :quiz_id, :uniqueness => { :scope => :student_id }
validate :student_teacher
belongs_to :quiz
belongs_to :student
has_many :question_responses, :dependent => :destroy, :inverse_of => :quiz_result
accepts_nested_attributes_for :question_responses
end
Any suggestions for how I can get this working?
server output:
Started POST "/quiz_results" for 127.0.0.1 at 2012-03-20 10:20:24 +0000
Processing by QuizResultsController#create as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"YceTVCh/GxdReb3KRMNj+cJhm6k0jwhsHl3LcJlSDJM=", "quiz_result"=>{"student_id"=>"695735877", "quiz_id"=>"17663260", "question_responses_attributes"=>{"0"=>{"question_id"=>"14376743", "answer"=>"test"}}}}
User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1 [["id", 113461968]]
Student Load (0.2ms) SELECT "students".* FROM "students" WHERE "students"."id" = 695735877 LIMIT 1
Teacher Load (0.4ms) SELECT "teachers".* FROM "teachers" WHERE "teachers"."id" = 657318460 LIMIT 1
(0.1ms) begin transaction
Quiz Load (0.4ms) SELECT "quizzes".* FROM "quizzes" WHERE "quizzes"."id" = 17663260 LIMIT 1
CACHE (0.0ms) SELECT "students".* FROM "students" WHERE "students"."id" = 695735877 LIMIT 1
QuizResult Exists (0.1ms) SELECT 1 FROM "quiz_results" WHERE ("quiz_results"."quiz_id" = 17663260 AND "quiz_results"."student_id" = 695735877) LIMIT 1
CACHE (0.0ms) SELECT "teachers".* FROM "teachers" WHERE "teachers"."id" = 657318460 LIMIT 1
CACHE (0.0ms) SELECT "teachers".* FROM "teachers" WHERE "teachers"."id" = 657318460 LIMIT 1
Question Load (0.1ms) SELECT "questions".* FROM "questions" WHERE "questions"."id" = 14376743 LIMIT 1
SQL (0.3ms) INSERT INTO "quiz_results" ("created_at", "message", "quiz_id", "student_id", "updated_at") VALUES (?, ?, ?, ?, ?) [["created_at", Tue, 20 Mar 2012 10:20:24 UTC +00:00], ["message", nil], ["quiz_id", 17663260], ["student_id", 695735877], ["updated_at", Tue, 20 Mar 2012 10:20:24 UTC +00:00]]
(0.1ms) rollback transaction
QuestionResponse Load (0.2ms) SELECT "question_responses".* FROM "question_responses" WHERE "question_responses"."quiz_result_id" IS NULL
Rendered quiz_results/_form.html.haml (5.0ms)
Rendered quiz_results/new.html.haml within layouts/application (6.8ms)
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1 [["id", 113461968]]
CACHE (0.0ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1 [["id", 113461968]]
Student Load (0.1ms) SELECT "students".* FROM "students" WHERE "students"."id" = 695735877 LIMIT 1
Completed 200 OK in 37ms (Views: 13.4ms | ActiveRecord: 2.4ms)
Use validates_associated: http://guides.rubyonrails.org/active_record_validations_callbacks.html#validates_associated
I'm experiencing some pretty slow load times with one of the pages of my Rails app. I have made some headway in my attempts to improve performance, but not as much as I had hoped. I'm wondering if I'm doing something silly to cause myself this grief or if anyone could offer me advice to better optimize my models/migrations/queries/views. I'm running Rails 3.1 and using PostgreSQL. This is what I've tried so far:
added includes(:items => :category) to the controller's query, as an attempt to fetch all the ItemCategories in advance, rather than running additional queries when outfits_helper requires them.
rendering partials as collections, rather than iterating through arrays/relations manually
added indexes to tables that are being accessed by the problematic controller action / view
outfits table: indexes for user_id and category_id
items table: indexes for user_id and category_id
outfit_items table: indexes for item_id, outfit_id, and [outfit_id, item_id]
Below are the relevant parts of my code:
Controller
# outfits_controller.rb
NUM_OUTFITS_PER_PAGE = 8
def other_users_outfits
# using Kaminari for pagination
#outfits = Outfit.does_not_belong_to_user(current_user).in_category_with_id(category_id).includes(:items => :category).page(params[:page]).per(NUM_OUTFITS_PER_PAGE)
end
Models
# outfit.rb
has_many :items, :through => :outfit_items
belongs_to :category, :class_name => 'OutfitCategory'
scope :does_not_belong_to_user, proc {|user| where('user_id != ?', user.id) }
scope :in_category_with_id, proc {|cat_id|
if cat_id.blank?
scoped
else
where(:category_id => cat_id)
end
}
.
# outfit_item.rb
belongs_to :item
belongs_to :outfit
.
# item.rb
has_many :outfit_items
has_many :outfits, :through => :outfit_items
belongs_to :category, :class_name => 'ItemCategory'
.
# item_category.rb
has_many :items, :foreign_key => 'category_id'
.
outfit_category.rb
has_many :outfits, :foreign_key => 'outfit_id'
Migrations
# create_outfits.rb
def self.up
create_table :outfits do |t|
t.column :category_id, :integer
t.column :user_id, :integer
t.timestamps
end
add_index :outfits, :category_id
add_index :outfits, :user_id
end
.
# create_outfit_items.rb
def self.up
create_table :outfit_items, :id => false do |t|
t.column :item_id, :integer
t.column :outfit_id, :integer
t.timestamps
end
add_index :outfit_items, :item_id
add_index :outfit_items, :outfit_id
add_index :outfit_items, [:outfit_id, :item_id]
end
.
# create_items.rb
def self.up
create_table :items do |t|
t.column :category_id, :integer
t.column :user_id, :integer
t.timestamps
end
add_index :items, :category_id
add_index :items, :user_id
end
.
# create_item_categories.rb
def self.up
create_table :item_categories do |t|
t.column :name, :string
t.column :category_type, :string
t.timestamps
end
end
.
# create_outfit_categories.rb
def self.up
create_table :outfit_categories do |t|
t.column :name, :string, :limit => 100, :null => false
t.timestamps
end
end
Views
# other_users_outfits.html.haml
= render :partial => 'other_users_outfit', :collection => outfits, :as => :outfit
.
# _other_users_outfit.html.haml
.outfit
.primary-items
= render :partial => 'other_users_outfit_primary_item', :collection => primary_outfit_items_top_to_bottom(outfit), :as => :item
.secondary-items
= render :partial => 'other_users_outfit_secondary_item', :collection => secondary_outfit_items(outfit), :as => :item
.
# _other_users_outfit_primary_item.html.haml
= image_tag item.image_url(:outfit_item), :class => 'primary-outfit-item'
.
# _other_users_outfit_secondary_item.html.haml
= image_tag item.image_url(:baby_thumb), :class => 'secondary-outfit-item'
Helper
# outfits_helper.rb
def primary_outfit_items_top_to_bottom(outfit)
primary_items = []
primary_items.push(outfit_item_in_category(outfit, 'Tops'))
primary_items.push(outfit_item_in_category(outfit, 'Full-lengths'))
primary_items.push(outfit_item_in_category(outfit, 'Bottoms'))
primary_items.push(outfit_item_in_category(outfit, 'Footwear'))
primary_items.compact
end
def secondary_outfit_items(outfit)
outfit.items.select{|item| item.category.category_type == 'secondary' }
end
def outfit_item_in_category(outfit, cat_name)
outfit.items.select{|item| item.category.name == cat_name }.first
end
Console Output
User Load (1.0ms) SELECT DISTINCT users.id, users.* FROM "users" WHERE "users"."id" = 3 LIMIT 1
(0.5ms) SELECT COUNT(count_column) FROM (SELECT 1 AS count_column FROM "outfits" WHERE (user_id != 3) LIMIT 8 OFFSET 0) subquery_for_count
Outfit Load (0.7ms) SELECT DISTINCT outfits.id, outfits.* FROM "outfits" WHERE (user_id != 3) ORDER BY outfits.created_at DESC LIMIT 8 OFFSET 0
OutfitItem Load (0.6ms) SELECT "outfit_items".* FROM "outfit_items" WHERE "outfit_items"."outfit_id" IN (28, 27, 26, 25, 24, 23, 22, 21)
Item Load (2.2ms) SELECT DISTINCT items.id, items.*, "items".* FROM "items" WHERE "items"."id" IN (18, 20, 23, 7, 6, 30, 4, 1, 17, 5, 15, 12, 9, 29, 10, 19, 3, 8, 13) ORDER BY items.created_at DESC
ItemCategory Load (0.5ms) SELECT "item_categories".* FROM "item_categories" WHERE "item_categories"."id" IN (4, 6, 2, 1, 3, 7)
Rendered outfits/_other_users_outfit_secondary_item.html.haml (1.2ms)
Rendered outfits/_other_users_outfit_secondary_item.html.haml (0.1ms)
Rendered outfits/_other_users_outfit_primary_item.html.haml (2.8ms)
Rendered outfits/_other_users_outfit_secondary_item.html.haml (1.4ms)
Rendered outfits/_other_users_outfit_secondary_item.html.haml (0.9ms)
Rendered outfits/_other_users_outfit_primary_item.html.haml (1.2ms)
Rendered outfits/_other_users_outfit_secondary_item.html.haml (0.3ms)
Rendered outfits/_other_users_outfit_secondary_item.html.haml (0.9ms)
Rendered outfits/_other_users_outfit_primary_item.html.haml (2.7ms)
Rendered outfits/_other_users_outfit_secondary_item.html.haml (0.8ms)
Rendered outfits/_other_users_outfit_secondary_item.html.haml (0.7ms)
Rendered outfits/_other_users_outfit_primary_item.html.haml (1.2ms)
Rendered outfits/_other_users_outfit_secondary_item.html.haml (1.1ms)
Rendered outfits/_other_users_outfit_secondary_item.html.haml (0.3ms)
Rendered outfits/_other_users_outfit_primary_item.html.haml (2.2ms)
Rendered outfits/_other_users_outfit_secondary_item.html.haml (0.8ms)
Rendered outfits/_other_users_outfit_secondary_item.html.haml (0.3ms)
Rendered outfits/_other_users_outfit_primary_item.html.haml (3.4ms)
Rendered outfits/_other_users_outfit_secondary_item.html.haml (0.3ms)
Rendered outfits/_other_users_outfit_secondary_item.html.haml (0.0ms)
Rendered outfits/_other_users_outfit_primary_item.html.haml (2.6ms)
Rendered outfits/_other_users_outfit_secondary_item.html.haml (0.3ms)
Rendered outfits/_other_users_outfit_secondary_item.html.haml (0.3ms)
Rendered outfits/_other_users_outfit_primary_item.html.haml (2.1ms)
Rendered outfits/_other_users_outfit.html.haml (56.3ms)
(0.5ms) SELECT COUNT(*) FROM "outfits" WHERE (user_id != 3)
Rendered outfits/other_users.html.haml within layouts/application (2073.5ms)
Role Load (0.4ms) SELECT "roles".* FROM "roles" WHERE "roles"."name" = 'admin' LIMIT 1
(0.4ms) SELECT 1 FROM "roles" INNER JOIN "roles_users" ON "roles"."id" = "roles_users"."role_id" WHERE "roles_users"."user_id" = 3 AND "roles"."id" = 1 LIMIT 1
Rendered layouts/_top_nav.html.haml (1.0ms)
(0.6ms) SELECT COUNT(*) FROM "items" WHERE "items"."user_id" = 3
Rendered layouts/_points_display.html.haml (5.7ms)
Outfit Load (0.6ms) SELECT DISTINCT outfits.id, outfits.* FROM "outfits" WHERE "outfits"."user_id" = 3 ORDER BY outfits.created_at DESC
OutfitCategory Load (0.3ms) SELECT "outfit_categories".* FROM "outfit_categories"
.
Any help would be much appreciated!
The only thing I see that could stand improvement is the code in the helper. Is there a reason why you're doing this:
def outfit_item_in_category(outfit, cat_name)
outfit.items.select{|item| item.category.name == cat_name }.first
end
instead of pushing this code to a scope in the Item model? If you have a lot of items, you're basically doing a SELECT * on them and then filtering in Ruby.