CounterCache not working for polymorphic association - ruby-on-rails

I am implementing counter_cache concept to my Forums app. It is working fine for one model that has a simple belongs_to association but not working for a polymorphic association.
My app structure is like this. I have 3 models, Forum, Post and Comment.
class Forum < ApplicationRecord
has_many :posts
end
Post Model:
class Post < ApplicationRecord
belongs_to :forum, counter_cache: true
has_many :comments, as: :parent
end
Comment Model:
class Comment < ApplicationRecord
belongs_to :parent,polymorphic: true, counter_cache: true
has_many :comments, as: :parent
end
My Comments Model is basically a polymorphic one, so a comment can belong to a post or a comment can belong to another comment (in this way it will be considered a reply of a comment)
I have a posts_count field in Forum model which is working fine and auto-incrementing and decrementing is working.
I also have a comments_count field in Post model.
Whenever a new comment is created, the comments_count field is incremented in the associated Post.
But when I try to create a comment whose parent (polymorphic association) is another comment (so basically a reply of a comment), I hit an error:
Started POST "/comments" for 103.255.4.86 at 2018-10-18 20:48:39 +0000
Cannot render console from 103.255.4.86! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255
Processing by CommentsController#create as JS
Parameters: {"utf8"=>"✓", "comment"=>{"body"=>"testing a reply", "parent_id"=>"812", "parent_type"=>"Comment"}}
Post Load (0.8ms) SELECT "posts".* FROM "posts" WHERE "posts"."id" = $1 LIMIT $2 [["id", 7], ["LIMIT", 1]]
(0.4ms) BEGIN
Comment Load (1.6ms) SELECT "comments".* FROM "comments" WHERE "comments"."id" = $1 LIMIT $2 [["id", 812], ["LIMIT", 1]]
SQL (0.9ms) INSERT INTO "comments" ("body", "parent_type", "parent_id", "owner_type", "owner_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING "id" [["body", "testing a reply"], ["parent_type", "Comment"], ["parent_id", 812], ["owner_type", "User"], ["owner_id", 46], ["created_at", "2018-10-18 20:48:39.141170"], ["updated_at", "2018-10-18 20:48:39.141170"]]
(0.4ms) ROLLBACK
Completed 500 in 43ms (ActiveRecord: 7.2ms)
ActiveModel::MissingAttributeError (can't write unknown attribute `comments_count`):
What am I missing here? Any hint would be really appreciated, Thanks!!

I have got it working by removing counter_cache: true from Comment model and defining my own methods for increment and decrement of the counter in Comment model. So here is my final Comment model:
class Comment < ApplicationRecord
belongs_to :parent,polymorphic: true,touch: true
has_many :comments,dependent: :destroy,as: :parent
after_create_commit { self.parent_post.increment!(:answers_count,1) }
after_destroy { self.parent_post.decrement!(:answers_count,1) }
def parent_post
(self.parent if parent_type == "Post") || self.parent.parent
end
end
If anyone comes up with another answer please post it here. Thanks.

The counter_culture counter cache gem supports polymorphic associations natively: https://github.com/magnusvk/counter_culture

Related

Rails Eager loading not loading nested associative records though included

I have three classes as follows
Class User < ActiveRecord
has_many :addresses
end
Class Address < ActiveRecord
belongs_to :country
end
Class Country < ActiveRecord
has_many :addresses
end
I am trying to eager load all the nested associative records by the following command
User.includes(addresses: :country)
But with this on rails console, only User and addresses get loaded but not the country. I am not sure what I am missing.
You need to use a hash to declare eager loading of nested resources, e.g.
User.includes(addresses: [:country])
instead of
User.includes(addresses: :country)
See the docs
Here's a full working example (Rails 6)
class Person < ApplicationRecord
has_many :projects
end
class Company
has_many :projects
end
class Project < ApplicationRecord
belongs_to :person
belongs_to :company
end
Then, on the console:
ActiveRecord::Base.logger = Logger.new(STDOUT)
me = Person.includes(projects: [:company]).find 4
Person Load (0.3ms) SELECT "people".* FROM "people" WHERE "people"."id" = $1 LIMIT $2 [["id", 4], ["LIMIT", 1]]
Project Load (0.4ms) SELECT "projects".* FROM "projects" WHERE "projects"."person_id" = $1 [["person_id", 4]]
Company Load (0.5ms) SELECT "companies".* FROM "companies" WHERE "companies"."id" IN ($1, $2, $3, $4, $5 ...)
me.projects.first.company.name
=> "my first project's customer name"
Note that there is no query to the database between asking for the name of the customer of my first project and the production of the return value. If there would have been one, the query would have been printed by the logger that pipes it output to STDOUT.

Create action in controller is omitting reference field

I have a problem with my rails application when i create a customer for an environment, the action itself if omitting the parameter environment_id in the insert statment.
This is my customer.rb model:
class Customer < ActiveRecord::Base
belongs_to :environment, inverse_of: :customers
has_many :all_services, class_name: 'Service'
has_many :services, inverse_of: :customer
has_paper_trail ignore: %i[created_at updated_at]
end
This is my environment.rb model:
class Environment < ActiveRecord::Base
has_many :all_customers, class_name: 'Customer', dependent: :destroy
has_many :customers, inverse_of: :environment
has_many :all_services, class_name: 'Service', dependent: :destroy
has_many :services, inverse_of: :environment
has_many :all_versions, class_name: 'Version', dependent: :destroy
has_many :versions, inverse_of: :environment
has_many :all_role_permissions, class_name: 'RolePermission', dependent: :destroy
has_many :role_permissions, inverse_of: :environment
has_paper_trail ignore: %i[created_at updated_at]
end
This is the customer_controller.rb create action:
def create
if customer_params.permitted?
render json: Customer.create!(customer_params), status: :ok
else
render json: { message: Api::V1::INVALID_PARAMETERS }, status: :bad_request
end
end
def customer_params
params.require(:customer).permit(:environment_id, :full_name, :document_type, :document_value, :customer_type)
end
And this is the server log:
Started POST "/api/v1/customers" for 127.0.0.1 at 2021-06-02 18:18:44 +0100
Processing by Api::V1::CustomersController#create as HTML
Parameters: {"full_name"=>"cvbcv", "document_type"=>"bcvbc", "document_value"=>"vbcvbcv", "customer_type"=>"bcvbcvb", "environment_id"=>1, "customer"=>{"full_name"=>"cvbcv", "document_type"=>"bcvbc", "document_value"=>"vbcvbcv", "customer_type"=>"bcvbcvb", "environment_id"=>1}}
User Load (0.7ms) SELECT "users".* FROM "users" WHERE "users"."uid" = $1 LIMIT $2 [["uid", "test1#test.cl"], ["LIMIT", 1]]
(0.5ms) BEGIN
↳ app/services/api/customers.rb:20:in `create_customer'
Environment Load (0.5ms) SELECT "environments".* FROM "environments" WHERE "environments"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
↳ app/services/api/customers.rb:20:in `create_customer'
Customer Create (1.4ms) INSERT INTO "customers" ("full_name", "document_type", "document_value", "customer_type", "created_at", "updated_at") VALUES ($1, $2, $3, $4, $5, $6) RETURNING "id" [["full_name", "cvbcv"], ["document_type", "bcvbc"], ["document_value", "vbcvbcv"], ["customer_type", "bcvbcvb"], ["created_at", "2021-06-02 17:18:44.378509"], ["updated_at", "2021-06-02 17:18:44.378509"]]
↳ app/services/api/customers.rb:20:in `create_customer'
(1.2ms) ROLLBACK
↳ app/services/api/customers.rb:20:in `create_customer'
Completed 500 Internal Server Error in 22ms (ActiveRecord: 4.3ms | Allocations: 6384)
ActiveRecord::NotNullViolation (PG::NotNullViolation: ERROR: null value in column "environment_id" violates not-null constraint
DETAIL: Failing row contains (28, cvbcv, bcvbc, vbcvbcv, bcvbcvb, null, 2021-06-02 17:18:44.378509, 2021-06-02 17:18:44.378509).
):
I tried everything i can possible know, i triple checked the migrations and schema.
I also checked active record gem version from previous projects and everything is ok.
One thing that might be relevant, this that the project have a front-end in Angular.
Rails validates for Environment presence in the database because Customer belongs_to to it.
Query below in your log is trying to load Environment with ID=1:
Environment Load (0.5ms) SELECT "environments".* FROM "environments" WHERE "environments"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
After this Rails try to INSERT new Customer without Environment reference because it seems that Environment with ID=1 doesn't exist in the database.
multiple ways of solving it.
problem: the environment_id is nil and already as not nil defined in the postgres DB.
you can add a required: true to the association, which rails then checks for existence. in your case a validation error would throw.
you can add a validates :enviroment_id, presence: true which would do the same
you merge the id into the params
example to always merge the environment_id into the params
def customer_params
params.require(:customer).permit(:full_name, :document_type, :document_value, :customer_type).merge(environment_id: GET_ME_THE_ENV_ID)
end

can't write unknown attribute "parent_id", has_one and belongs_to

i'm using rails 4 and was wondering if anyone could find what's wrong in my code.
I have project model and I created a team model that has a belongs_to - has_one relation with project.
project model:
class CrmProject < ActiveRecord::Base
has_one :crm_team
team model
class CrmTeam < ActiveRecord::Base
belongs_to :crm_project
accepts_nested_attributes_for :crm_project
belongs_to :crm_section
belongs_to :manager, class_name: "User"
has_many :users
accepts_nested_attributes_for :users
end
When submitting the form to create a new team i get this error:
ActiveModel::MissingAttributeError in CrmTeamsController#create
can't write unknown attribute `crm_team_id`
and log from server :
Parameters: {"crm_team"=>{"crm_project"=>"5", "manager"=>"3", "user_ids"=>["", "2"]}, "co
mmit"=>"Create Crm team"}
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? ORDER BY "users"."id" ASC LIMIT 1
[["id", 1]]
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1 [["id", 2]]
(0.1ms) begin transaction
User Exists (0.2ms) SELECT 1 AS one FROM "users" WHERE ("users"."email" = 'email#gmail.com' AND "users"."
id" != 2) LIMIT 1
SQL (0.2ms) INSERT INTO "crm_teams" ("created_at", "updated_at") VALUES (?, ?) [["created_at", "2020-03-1
1 07:29:19.735115"], ["updated_at", "2020-03-11 07:29:19.735115"]]
(0.2ms) rollback transaction
Completed 500 Internal Server Error in 9ms (ActiveRecord: 1.0ms)
When you use a has_one relation like this
has_one :crm_team
Rails expects you to add crm_team_id to your crm_project, in order to understand which crm_team is related with crm_project object. Adding it going to solve your problem.
In my opinion defining opposite like this much better in logic. Because in future these teams can have multiple projects.
class CrmProject < ActiveRecord::Base
belongs_to :crm_team
class CrmTeam < ActiveRecord::Base
***has_one/has_many(pick one)*** :crm_project
accepts_nested_attributes_for :crm_project
belongs_to :crm_section
belongs_to :manager, class_name: "User"
has_many :users
accepts_nested_attributes_for :users
end

Using `assign_attributes` saves `has_many through:` association immediately

As far as I know, assign_attributes (unlike update_attributes) is not supposed to save the record or for that matter, any record.
So it quite startled me when I discovered that this is not true when supplying _ids for a has_many through: relation.
Consider the following example:
class GroupUser < ApplicationRecord
belongs_to :group
belongs_to :user
end
class Group < ApplicationRecord
has_many :group_users
has_many :users, through: :group_users
end
class User < ApplicationRecord
has_many :group_users
has_many :groups, through: :group_users
validates :username, presence: true
end
So we have users and groups in an m-to-m relationship.
Group.create # Create group with ID 1
Group.create # Create group with ID 2
u = User.create(username: 'Johny')
# The following line inserts two `GroupUser` join objects, despite the fact
# that we have called `assign_attributes` instead of `update_attributes`
# and, equally disturbing, the user object is not even valid as we've
# supplied an empty `username` attribute.
u.assign_attributes(username: '', group_ids: [1, 26])
The log as requested by a commenter:
irb(main):013:0> u.assign_attributes(username: '', group_ids: [1, 2])
Group Load (0.2ms) SELECT "groups".* FROM "groups" WHERE "groups"."id" IN (1, 2)
Group Load (0.1ms) SELECT "groups".* FROM "groups" INNER JOIN "group_users" ON "groups"."id" = "group_users"."group_id" WHERE "group_users"."user_id" = ? [["user_id", 1]]
(0.0ms) begin transaction
SQL (0.3ms) INSERT INTO "group_users" ("group_id", "user_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["group_id", 1], ["user_id", 1], ["created_at", "2017-06-29 08:15:11.691941"], ["updated_at", "2017-06-29 08:15:11.691941"]]
SQL (0.1ms) INSERT INTO "group_users" ("group_id", "user_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["group_id", 2], ["user_id", 1], ["created_at", "2017-06-29 08:15:11.693984"], ["updated_at", "2017-06-29 08:15:11.693984"]]
(2.5ms) commit transaction
=> nil
I daresay that update_attributes and the _ids construct are mostly used for processing web forms - in this case a form that updates the user itself as well as its group association. So I think it is quite safe to say that the general assumption here is all or nothing, and not a partial save.
Am I using it wrong in some way?
#gokul-m suggests reading about the issue at https://github.com/rails/rails/issues/17368. One of the comments in there points to a temporary workaround: https://gist.github.com/remofritzsche/4204e399e547ff7e3afdd0d89a5aaf3e
an example of my solution to this problem:
ruby:
def assign_parameters(attributes, options = {})
with_transaction_returning_status {self.assign_attributes(attributes, options)}
end
You can handle validation with assign_attributes like so
#item.assign_attributes{ year: "2021", type: "bad" }.valid?

Rails 3 ActiveRecord has_many polymorphic - Why isn't the id of the first object being saved for insertion into the join table?

Wise people,
Please help me troubleshoot my has_many polymorphic object creation problem.
I have a polymorphic has_many relationship between two objects with a join table in the middle. Acronyms (and other objects) on one side, Categories on the other. In between them, I have a join object between the two objects.
I cannot successfully create an acronym with a category. I can, however, create an acronym without a category, then add the category to the existing acronym.
My model objects look like the following code:
class Acronym < ActiveRecord::Base
has_many :category_belongings, :as => :categorizable, :dependent => :delete_all
has_many :categories, :through => :category_belongings
end
class Category < ActiveRecord::Base
has_many :category_belongings, :dependent => :delete_all
has_many :acronyms, :through => :category_belongings, :source => :categorizable, :source_type => 'Acronym'
end
class CategoryBelonging < ActiveRecord::Base
belongs_to :categorizable, :polymorphic => true
belongs_to :category
end
For reference, I removed some extra fields and irrelevant rules. I also refactored my code to look just like the example here:
Setting up a polymorphic has_many :through relationship
Through my Rails application/rails console, I am able to:
Successfully create an acronym without any categories
Successfully create a category
Successfully add an existing category to an existing acronym through updating the acronym
I cannot, however, create a new acronym with an existing category. I can see in the logs Rails tries to do the following:
Gathers the parameters from the post. These parameters include the columns on the Acronym table as well as the category IDs
Checks to see if the Category exists. It does.
Checks to see if the Acronym exists. It does not.
Inserts a new Acronym into the database with all the column parameters
Attempts to insert a new entry in the join table. This is where the error is. The SQL for this insertion contains a Nil value for the Acronym ID.
Rolls my changes back.
Processing by Admin::AcronymsController#create as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"/uquz5FvtMh0QWP5NoWwTO9FMMEC9rsMTrTj4WUNxxE=", "acronym"=>{"name"=>"A Test Acronym", "definition"=>"A Test Definition", "explanation"=>"", "category_ids"=>["1", ""], "state"=>"unapproved"}, "commit"=>"Create Acronym"}
Category Load (0.4ms) SELECT "categories".* FROM "categories" WHERE "categories"."id" = $1 LIMIT 1 [["id", 1]]
(0.3ms) BEGIN
Category Exists (0.6ms) SELECT 1 AS one FROM "categories" WHERE ("categories"."name" = 'Internet' AND "categories"."id" != 1) LIMIT 1
Acronym Exists (26.0ms) SELECT 1 AS one FROM "acronyms" WHERE "acronyms"."name" = 'A Test Acronym' LIMIT 1
SQL (18.1ms) INSERT INTO "acronyms" ("definition", "explanation", "improvement_reason", "likes_count", "name", "state", "submitter_id") VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING "id" [["definition", "A Test Definition"], ["explanation", ""], ["improvement_reason", nil], ["likes_count", 0], ["name", "A Test Acronym"], ["state", "unapproved"], ["submitter_id", nil]]
SQL (55.8ms) INSERT INTO "category_belongings" ("categorizable_id", "categorizable_type", "category_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4, $5) RETURNING "id" [["categorizable_id", nil], ["categorizable_type", "Acronym"], ["category_id", 1], ["created_at", Thu, 28 Feb 2013 23:03:48 UTC +00:00], ["updated_at", Thu, 28 Feb 2013 23:03:48 UTC +00:00]]
PG::Error: ERROR: null value in column "categorizable_id" violates not-null constraint
: INSERT INTO "category_belongings" ("categorizable_id", "categorizable_type", "category_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4, $5) RETURNING "id"
(0.3ms) ROLLBACK
I would assume on the Acronym insert, an acronym_id would be created, but I don't see any output indicating what the value of that acronym_id is. To me, the error is clearly that the acronym_id is Nil when I try to insert the new row into category_belongings. Do I need to somehow point acronym_id to categorizable_id? If that's the case, how would I do this?
I'm new to rails, and I am trying to maintain and improve an existing system. Thanks for any help.
Based on what you are looking to do, you need to use accepts_nested_attributes_for. This will allow you to create a category when you create an acronym. Here is how you should set it up.
class Acronym < ActiveRecord::Base
has_many :categories, :through => :category_belongings
has_many :category_belongings, :dependent => :destroy
accepts_nested_attributes_for :acronyms
attr_accessible :acronyms_attributes
end
class Category < ActiveRecord::Base
has_many :acronyms, :through => :category_belongings
has_many :category_belongings, :dependent => :destroy
end
class CategoryBelonging < ActiveRecord::Base
belongs_to :acronym
belongs_to :category
end
This setup will allow you to nest your forms and create a category when you create an acronym. To read more about all the options accepts_nested_attributes_for has checkout the Rails API. This Railscast also has some good information on nested forms.
It seems my code was fine - I got bit by a Rails bug. I solved the problem by moving my rails version to 3.2.8 with the security patch enabled according to these instructions. After that, everything worked fine with no code changes necessary.
https://github.com/rails/rails/issues/8269#issuecomment-10518099

Resources