I'm using will_paginate to paginate posts within a group.
A group might have many posts and I don't want to show all posts at once.
For some strange reason I noticed that a lot of posts are deleted from the database when I do #group.posts = paginated_posts as displayed in * groups_controller*.
I'm not doing save on #group, why are the posts deleted?
Tests first
69 it "Pagination – get one group and its posts" do
70 10.times { Fabricate(:post, group: #group, user: #user) }
71 puts "POST COUNT BEFORE #{#group.posts.count}"
72 # byebug
73 get group_path(#group, posts_per_page: 3)
74 puts "POST COUNT AFTER #{#group.posts.count}"
75 expect(response).to have_http_status(200)
76 expect(#group).to be_present
77 end
Output from the test
Groups
GET /group/:group_id?posts_per_page=10&posts_page=2
POST COUNT BEFORE 12
POST COUNT AFTER 3
groups_controller.rb
9 def show
10 paginated_posts = #group.posts.paginate(
11 page: params[:posts_page],
12 per_page: params[:posts_per_page] || 100000,
13 )
14 #group.posts = paginated_posts
15 render json: #group
16 end
The log
(0.3ms) RELEASE SAVEPOINT active_record_1
Started GET "/groups/33?posts_per_page=3" for 127.0.0.1 at 2018-09-18 12:52:13 +0000
Processing by GroupsController#show as HTML
Parameters: {"posts_per_page"=>"3", "id"=>"33"}
User Load (2.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", "78913bd2-4c72-4c73-ba75-421e154c830b"], ["LIMIT", 1]]
Group Load (1.4ms) SELECT "groups".* FROM "groups" WHERE "groups"."id" = $1 LIMIT $2 [["id", 33], ["LIMIT", 1]]
Post Load (2.2ms) SELECT "posts".* FROM "posts" WHERE "posts"."group_id" = $1 LIMIT $2 OFFSET $3 [["group_id", 33], ["LIMIT", 3], ["OFFSET", 0]]
Post Load (0.3ms) SELECT "posts".* FROM "posts" WHERE "posts"."group_id" = $1 [["group_id", 33]]
(0.4ms) SAVEPOINT active_record_1
Comment Load (0.5ms) SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = $1 [["post_id", 156]]
PostImage Load (0.3ms) SELECT "post_images".* FROM "post_images" WHERE "post_images"."post_id" = $1 [["post_id", 156]]
Post Destroy (0.3ms) DELETE FROM "posts" WHERE "posts"."id" = $1 [["id", 156]]
Comment Load (0.3ms) SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = $1 [["post_id", 157]]
PostImage Load (0.3ms) SELECT "post_images".* FROM "post_images" WHERE "post_images"."post_id" = $1 [["post_id", 157]]
Post Destroy (0.3ms) DELETE FROM "posts" WHERE "posts"."id" = $1 [["id", 157]]
Comment Load (0.3ms) SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = $1 [["post_id", 158]]
#group.posts = ... doesn't work the way you think it does. If you have a one-to-many relationship and you assign an array of associated records to the existing group object, the records set and remove the group.id foreign key immediately. No save is required.
So when you do
#group.posts = paginated_posts
The paginated posts are assigned to the association and all other records in the association are removed from the association. Instantly. They're not necessarily deleted, but they no longer belong to #post
You may want to change your #to_json ... perhaps add
attr_accessor :page_of_posts
And then in your controller
#group.page_of_posts = paginated_posts
And ensure page_of_posts is what's rendered in the json. Others may suggest a better way.
Related
I am going back to refresh my Rails knowledge by watching some tutorials, and I came across where the tutorial rails app uses includes() on index.
def index
#books = Book.all
end
vs
def index
#books = Book.includes(:author, :genre)
end
As a side note, book belongs_to author and genre. Author has_many books and genre also has_many books.
When all is used, it looks like this when I refresh page:
Rendering books/index.html.erb within layouts/application
Book Load (1.4ms) SELECT "books".* FROM "books"
Author Load (0.3ms) SELECT "authors".* FROM "authors" WHERE "authors"."id" = $1 LIMIT $2 [["id", 2], ["LIMIT", 1]]
Genre Load (0.3ms) SELECT "genres".* FROM "genres" WHERE "genres"."id" = $1 LIMIT $2 [["id", 2], ["LIMIT", 1]]
Author Load (0.4ms) SELECT "authors".* FROM "authors" WHERE "authors"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
Genre Load (0.3ms) SELECT "genres".* FROM "genres" WHERE "genres"."id" = $1 LIMIT $2 [["id", 3], ["LIMIT", 1]]
CACHE (0.0ms) SELECT "authors".* FROM "authors" WHERE "authors"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
CACHE (0.0ms) SELECT "genres".* FROM "genres" WHERE "genres"."id" = $1 LIMIT $2 [["id", 3], ["LIMIT", 1]]
CACHE (0.0ms) SELECT "authors".* FROM "authors" WHERE "authors"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
CACHE (0.0ms) SELECT "genres".* FROM "genres" WHERE "genres"."id" = $1 LIMIT $2 [["id", 2], ["LIMIT", 1]]
When includes is used, when I reload the page it shows:
Rendering books/index.html.erb within layouts/application
Book Load (0.4ms) SELECT "books".* FROM "books"
Author Load (0.5ms) SELECT "authors".* FROM "authors" WHERE "authors"."id" IN (2, 1)
Genre Load (0.4ms) SELECT "genres".* FROM "genres" WHERE "genres"."id" IN (2, 3)
I think this makes includes far more efficient than all because it hits the entire model database.
My question is, why do people still use all? Why not completely eradicate all and use includes from now on? Is there any situation where I would prefer to use all and not use includes? I am using Rails 5.0.1.
Let me talk a little bit about includes.
Suppose you need to get the user name of first five post. You quickly write the query below and go enjoy your weekend.
posts = Post.limit(5)
posts.each do |post|
puts post.user.name
end
Good. But let's look at the queries
Post Load (0.5ms) SELECT `posts`.* FROM `posts` LIMIT 5
User Load (0.3ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1
User Load (0.3ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1
User Load (0.3ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 2 LIMIT 1
User Load (0.3ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 2 LIMIT 1
User Load (0.3ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1
1 query to fetch all posts and 1 query to fetch users for each post results in a total of 6 queries. Check out the solution below which does the same thing, just in 2 queries:
posts = Post.includes(:user).limit(5)
posts.each do |post|
puts post.user.name
end
#####
Post Load (0.3ms) SELECT `posts`.* FROM `posts` LIMIT 5
User Load (0.3ms) SELECT `users`.* FROM `users` WHERE `users`.`id` IN (1, 2)
There’s one little difference. Add includes(:posts) to your query, and problem solved. Quick, nice, and easy.
But don’t just add includes in your query without understanding it properly. Using includes with joins might result in cross-joins depending on the situation, and you don’t need that in most cases.
If you want to add conditions to your included models you’ll have to explicitly reference them. For example:
User.includes(:posts).where('posts.name = ?', 'example')
Will throw an error, but this will work:
User.includes(:posts).where('posts.name = ?', 'example').references(:posts)
Note that includes works with association names while references needs the actual table name.
I'm trying to do what I think should be simple: do a simple edit on a single text string field with the default update action. But it just doesn't seem to work, despite many attempts and alterations.
There are no errors and the flash message responds successfully, but information isn't saved to the database at all:
routes.rb
resources :interviews do
resources :invitations do
put :accept
end
end
views/invitations/edit.html.haml
= simple_form_for [#interview, #invitation] do |f|
= f.error_notification
= f.input :testing
= f.submit 'Edit Invitstion', :class => 'button small'
controllers/invitations_controller.rb
def update
#invitation = Invitation.find(params[:id])
#interview = Interview.find(params[:interview_id])
#invitation.update_attributes(invitation_params)
if #invitation.update_attributes(invitation_params)
redirect_to edit_interview_invitation_path(#interview, #invitation), notice: "Your profile has been successfully updated."
else
render action: "edit"
end
end
private
def invitation_params
params.permit(:user_id, :interview_id, :invitation_id, :session_time, :workflow_state, :testing)
end
And here's the log:
Started PATCH "/interviews/3/invitations/7" for ::1 at 2016-05-15 19:01:52 +0800
Processing by InvitationsController#update as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"o0U5t0yPN0aE2er+DWK0uxqRGyp4ywfdSrEfvwiSQ3UUaOnr3Fd0raFs1IUqVzizKoqxRU0DDpmvysntB9fdhQ==", "invitation"=>{"interview_id"=>"3", "workflow_state"=>"invited", "session_time"=>"", "testing"=>"testtesttest"}, "commit"=>"Edit Invitstion", "interview_id"=>"3", "id"=>"7"}
User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 ORDER BY "users"."id" ASC LIMIT 1 [["id", 7]]
Invitation Load (0.2ms) SELECT "invitations".* FROM "invitations" WHERE "invitations"."id" = $1 LIMIT 1 [["id", 7]]
Role Load (0.2ms) SELECT "roles".* FROM "roles" WHERE "roles"."id" = $1 LIMIT 1 [["id", 3]]
Interview Load (0.2ms) SELECT "interviews".* FROM "interviews" WHERE "interviews"."id" = $1 ORDER BY created_at DESC LIMIT 1 [["id", 3]]
CACHE (0.0ms) SELECT "invitations".* FROM "invitations" WHERE "invitations"."id" = $1 LIMIT 1 [["id", "7"]]
CACHE (0.0ms) SELECT "interviews".* FROM "interviews" WHERE "interviews"."id" = $1 ORDER BY created_at DESC LIMIT 1 [["id", "3"]]
Unpermitted parameters: utf8, _method, authenticity_token, invitation, commit, id
(0.1ms) BEGIN
Invitation Exists (0.4ms) SELECT 1 AS one FROM "invitations" WHERE ("invitations"."user_id" = 3 AND "invitations"."id" != 7 AND "invitations"."interview_id" = 3) LIMIT 1
(0.1ms) COMMIT
Redirected to http://localhost:3000/interviews/3/invitations/7/edit
Completed 302 Found in 12ms (ActiveRecord: 1.6ms)
Started GET "/interviews/3/invitations/7/edit" for ::1 at 2016-05-15 19:01:52 +0800
Processing by InvitationsController#edit as HTML
Parameters: {"interview_id"=>"3", "id"=>"7"}
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 ORDER BY "users"."id" ASC LIMIT 1 [["id", 7]]
Invitation Load (0.3ms) SELECT "invitations".* FROM "invitations" WHERE "invitations"."id" = $1 LIMIT 1 [["id", 7]]
Role Load (0.2ms) SELECT "roles".* FROM "roles" WHERE "roles"."id" = $1 LIMIT 1 [["id", 3]]
Interview Load (0.2ms) SELECT "interviews".* FROM "interviews" WHERE "interviews"."id" = $1 ORDER BY created_at DESC LIMIT 1 [["id", 3]]
Rendered invitations/edit.html.haml within layouts/application (6.1ms)
Completed 200 OK in 48ms (Views: 39.1ms | ActiveRecord: 1.6ms)
Check the format of your params object in your logs. Your invitation params are being passed within the params["invitation"] key, yet you're whitelisting and updating your object based on the params in the root params hash.
Also note that your logs are reporting that you're trying to update your invitation with unpermitted params:
Unpermitted parameters: utf8, _method, authenticity_token, invitation, commit, id
You can fix this by simply updating your invitation_params to use params[:invitation] rather than params like so:
def invitation_params
params.require(:invitation).permit(:user_id, :interview_id, :invitation_id, :session_time, :workflow_state, :testing)
end
Also, you might want to consider raising an error if you're trying to update a parameter that's not whitelisted to prevent these sorts of issues in the future.
In your rails config:
config.action_controller.action_on_unpermitted_parameters = :raise
I'm trying to update the shop_id of a user when they click an add button.
The controller method called is this:
def update_shop
#user = User.find(params[:id])
#shop = Shop.find(params[:shop_id])
#user.update_attribute(:shop_id, params[:shop_id])
flash[:success] = "Added Shop!"
redirect_to #shop
end
The server when the button is clicked reads:
Started POST "/updateshop?id=3&shop_id=1" for ::1 at 2015-08-31 05:50:52 -0500
Processing by UsersController#update_shop as HTML
Parameters: {"authenticity_token"=>"jRozldw1u3TrWhaL6CeJyw4Tm5V5S/IFEQQulRkuV1Ot85kmPOsMa2jH2L6m8EFDpy7Ygc9SMBvPLJCuosHXUg==", "id"=>"3", "shop_id"=>"1"}
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1 [["id", 3]]
Shop Load (0.1ms) SELECT "shops".* FROM "shops" WHERE "shops"."id" = ? LIMIT 1 [["id", 1]]
(0.1ms) begin transaction
SQL (0.3ms) UPDATE "users" SET "shop_id" = ?, "updated_at" = ? WHERE "users"."id" = ? [["shop_id", 1], ["updated_at", "2015-08-31 10:50:52.089826"], ["id", 3]]
(5.8ms) commit transaction
But it doesn't actually update User.shop_id
And sometimes it doesn't have the update line, and reads:
Started POST "/updateshop?id=3&shop_id=1" for ::1 at 2015-08-31 05:56:54 -0500
Processing by UsersController#update_shop as HTML
Parameters: {"authenticity_token"=>"D1ODlfDnhJmQ9NUfh+GL2JE747nJC2t4eqOziRGNCaUvuikmEDkzhhNpGyrJNkNQOAagrX8SqWakiw2yqmKJpA==", "id"=>"3", "shop_id"=>"1"}
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1 [["id", 3]]
Shop Load (0.1ms) SELECT "shops".* FROM "shops" WHERE "shops"."id" = ? LIMIT 1 [["id", 1]]
(0.1ms) begin transaction
(0.1ms) commit transaction
The parameters are passed correctly and I think update_attribute is correct, what's going wrong?
The value is already assigned, this is why it's not updated. I mean, there is no actual changes, this is why no UPDATE statements were issued.
I have a table subscription with a column status. In my subscriptions controller I have a method accept_player that is supposed to update the subscription.status to "confirmed!"
def accept_player
#subscription = Subscription.find(params[:subscription_id_accept_player])
#subscription.status = "confirmed!"
#subscription.save
authorize #subscription
redirect_to tournament_subscriptions_path(#subscription.tournament)
end
unfortunately every time I try to trigger that method, a rollback seem to take place:
Started POST "/accept_player/39" for ::1 at 2015-07-08 22:01:21 +0100
ActiveRecord::SchemaMigration Load (12.4ms) SELECT "schema_migrations".* FROM "schema_migrations"
/Users/davidgeismar/code/davidgeismar/tennis-match/app/controllers/subscriptions_controller.rb:141: warning: duplicated key at line 155 ignored: "CardType"
Processing by SubscriptionsController#accept_player as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"aas8OPHBpvPwNbbmx/SVipsRM+eKo63nuVilMroxKcU9HRVonjSqEuH7aLY91gFi9PHMUsUqRqk7qhnv2m4L/A==", "subscription_id_accept_player"=>"39", "commit"=>"Confirmer ce Joueur", "subscription_id"=>"39"}
User Load (13.3ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 ORDER BY "users"."id" ASC LIMIT 1 [["id", 2]]
Subscription Load (11.6ms) SELECT "subscriptions".* FROM "subscriptions" WHERE "subscriptions"."id" = $1 LIMIT 1 [["id", 39]]
(5.7ms) BEGIN
Subscription Exists (0.8ms) SELECT 1 AS one FROM "subscriptions" WHERE ("subscriptions"."user_id" = 20 AND "subscriptions"."id" != 39 AND "subscriptions"."tournament_id" = 9) LIMIT 1
(12.6ms) ROLLBACK
Tournament Load (2.4ms) SELECT "tournaments".* FROM "tournaments" WHERE "tournaments"."id" = $1 LIMIT 1 [["id", 9]]
User Load (0.4ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT 1 [["id", 2]]
Redirected to http://localhost:3000/tournaments/9/subscriptions
Completed 302 Found in 246ms (ActiveRecord: 79.7ms)
Any ideas about what might be going wrong here ?
This code:
authorize #subscription
is probably causing the rollback. If you're in dev mode, just comment it out, reload!, and try to manually add a record and see if that's the cause.
Here is my problem:
I'm using Devise's guest_user, that contains a logging_in method to transfer guest_user parameters to the registered user when he logs in. So in my case, the user has_many periods, dependent: :destroy, so here is the logging_in method:
def logging_in
guest_periods = guest_user.periods.all
guest_periods.each do |p|
p.user_id = current_user.id
p.save!
end
current_user.latest_entry = guest_user.latest_entry
current_user.is_in_zone = guest_user.is_in_zone
current_user.save
end
However, when a guest_user logs in, his periods gets destroyed instead of being transfered. Here is the log:
Started GET "/" for ::1 at 2015-05-11 00:18:03 +0300
Processing by WelcomeController#index as HTML
User Load (0.4ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 ORDER BY "users"."id" ASC LIMIT 1 [["id", 24]]
User Load (0.4ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT 1 [["id", 23]]
Period Load (0.3ms) SELECT "periods".* FROM "periods" WHERE "periods"."user_id" = $1 [["user_id", 23]]
(0.2ms) BEGIN
CACHE (0.0ms) SELECT "periods".* FROM "periods" WHERE "periods"."user_id" = $1 [["user_id", 23]]
SQL (0.8ms) UPDATE "periods" SET "user_id" = $1, "updated_at" = $2 WHERE "periods"."id" = $3 [["user_id", 24], ["updated_at", "2015-05-10 21:18:03.863162"], ["id", 170]]
(0.9ms) COMMIT
(0.2ms) BEGIN
SQL (2.1ms) UPDATE "users" SET "is_in_zone" = $1, "latest_entry" = $2, "updated_at" = $3 WHERE "users"."id" = $4 [["is_in_zone", "t"], ["latest_entry", "2015-05-04"], ["updated_at", "2015-05-10 21:18:03.875572"], ["id", 24]]
(15.8ms) COMMIT
(0.5ms) BEGIN
SQL (0.3ms) DELETE FROM "periods" WHERE "periods"."id" = $1 [["id", 170]]
SQL (0.7ms) DELETE FROM "users" WHERE "users"."id" = $1 [["id", 23]]
(1.2ms) COMMIT
So we can see that the transfer is done, but then in the end, the periods are destroyed anyway. They should not be, as they are not belonging to the user to be destroyed any more.
Why is it happening?
Even though Period#user_id has changed, guest_user.periods is still loaded in memory and is what gets destroyed when you destroy the guest user. If you guest_user.reload, its associations will clear out and it becomes safe to destroy. You could also guest_user.periods(true) to force reload of just the periods.
Another option is:
guest_user.periods.update_all(user_id: current_user.id)
This executes a single query to perform the update, which will be nice if there are a lot of periods, and also doesn't load the guest_user.periods association, so it will load fresh during the destroy and find the correct empty set.