Rails has_many associations are not saved when only attributes are changed - ruby-on-rails

I have two models, Profile and Skill, where a profile has_many skills.
I am using Rails as an API and passing an array of skill objects.
I'm doing #profile.skills = #skills in the controller, where #skills is my data from the frontend.
Whenever I delete or add a new skill, the above works as expected - also, #profile.skills.replace(#skills) works just the same.
The associated object gets deleted or created in the database as well. All as expected.
However, if I only change one or more attributes on an already existing skill, the changes are not saved to the database.
If I log #profile.skills after the above line of code, it seems like the changes expected are present.
But it does not get saved to the database and on the next request the changes are obviously not present.
What am I doing wrong?

Have you tried to pass like this:
class Profile < ApplicationRecord
has_many :skills
accepts_nested_attributes_for :skills
end
or
class Profile < ApplicationRecord
has_many :skills, autosave: true
end
after that, edit the same attribute of the same record and save
#profile.skills.same.same_attr = new_val
#profile.save
accepts_nested_attributes_for - it also adds autosave to asociations, and a lot of other things, you can see more in the documentation and source code
https://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html
a little bit about how autosave works for associations
https://api.rubyonrails.org/classes/ActiveRecord/AutosaveAssociation.html

Related

Rails model associations and destroy method

I need some help to solve my problem.
I'm working on a project where I develop the manager's functions of the application. My problem is related with two Destroy functions and how they related with themselves:
Simplifying, I have 3 models (Test, Application and JoinTestJob) described below in code sample.
I want the manager be able to destroy a JoinTestJob's object only if this is related with any Application's objects. I need consistency in Application's objects. That is the reason I created a before_destroy method in JoinTestJob model. Until here is working fine.
I also want the manager be able to destroy a Test's object, and with that, the associated objects should be destroy too, as declared in Test model. So here start the problem:
When I delete a Test's object I can see in log that all Application's objects associated are deleted first. After that, should be JoinTestJob's objects to be deleted, but I got a rollback. I know the reason of the rollback is the before_destroy method in JoinTestJob (I can see it in log). Apparently, this method still can find Application's objects, even though I saw in log that they were deleted. In this case, it seems they really would be deleted after the transaction is done.
So, how can I accomplish to having those two features working?
Model:
class Test < ApplicationRecord
has_many :applications, :dependent => :destroy
has_many :persons, through: :applications
has_many :join_test_jobs, :dependent => :destroy
has_many :jobs, through: :join_test_jobs
class Application < ApplicationRecord
belongs_to :join_test_job
belongs_to :person
class JoinTestJob < ApplicationRecord
belongs_to :test
belongs_to :job
before_destroy :check_relation_between_application_and_join_test_job
def check_relation_between_application_and_join_test_job
if Application.find_by(join_test_job_id: "#{self.id}")
self.errors.add(:base, "It's not possible to delete this item, because there are some applications related with. Please, delete them first.")
throw(:abort)
end
end
Edit 1:
About the log requested in comments, it's really basic. I will ask to you guys ignore some details in the image. I had to translate the problem to English, so there are some Portuguese words in the image. What is important to observe in the log:
All Application's objects related to Test are deleted first. Then, when rails started to look up for JoinTestJob's objects you can see the rollback. Before the rollback, there is a last request from Application trigged by the before_destroy method, as you can see in Log's image. The throw(:abort) method is called in this last request.
Word's Translation:
Application/applications => Inscricao/inscricaos
JoinTestJob/join_test_jobs => JoinConcursoCargo/join_concurso_cargos
To clarify things, DadosPessoalCandidato and DadosPortadorDeficiencia are models associated with Application and they hold personal informations about who is applying for, like address and phone. They can be ignored.
Edit 2:
Adding more information about what represent the models, so you guys can understand the problem better.
Test model: It's like a Civil service exam, where people would apply for getting a job. Who gets the best score in the exam would be selected for the job. This model hold information as: when this exam will happen, documents related with the exam's rules, when the result of the exam will be release and etc.
JoinTestJob model: It joins the concept of a Job and the Test that are offering these jobs. Here, we could find information as: specificity of the job, like the period of work, how much hours of work per day, salary and etc.
Application model: This model hold information related with the person who is applying for the job and the Test that are offering the job. So, we could find here information as: what job the person apply for, how much he/she payed for applying, the date of the Test (exam) will be happening, personal informations as age, name, address. A person could apply for more than one job for the same Test.
I hope I can help you guys with these new informations.
I think you should change your models:
class Test < ApplicationRecord
has_many :applications, :dependent => :destroy
class Application < ApplicationRecord
has_one :join_test_job, dependent: :destroy
class JoinTestJob < ApplicationRecord
belongs_to :application
validates :application, presence: true
so, if a Test is destroyed, then applications relates with destroyed test will be destroyed, and then JoinTestJob relates with destroyed applications are also destroyed.
I assume JoinTestJob model is stored in join_test_jobs model and have application_id.
You can make field store application_id is NOT NULL at Database level, and add validates :application, presence: true code make sure that a JoinTestJob always has one related application.

Build method is not associating in Rails

I'm still newbie in Rails, but got confused with the initialization of a HABTM association. Reading its documentation, it says
When initializing a new has_one or belongs_to association you must use the build_ prefix to build the association, rather than the association.build method that would be used for has_many or has_and_belongs_to_many associations.
So, basically, let's suppose we have two models:
class User < ApplicationRecord
has_and_belongs_to_many :organizations
end
class Organization < ApplicationRecord
has_and_belongs_to_many :users
end
Inside organization_controller, since I'm using Devise, my create method should have something like this:
#organization = current_user.organizations.build(organization_params)
#organization.save
However, it is not working. With byebug, I checked that for the current_user.organizations, the new organization was there, but, if I call #organization.users, there's an empty array. Looks like it's required to run current_user.save as well, is it correct? I was able to associate both models with this code:
#organization = Organization.new(organization_params)
#organization.users << current_user
#organization.save
You should highly consider using has_many, :through as that's the preferred way to do these kinds of relationships now in Rails.
having said that if you want to use has_and_belongs_to_many associations yes its get stored in join table on main object save.

How to make associations work in built and unsaved objects?

Let's say I have something like:
#order = Order.new(status: :pending)
#item = #order.items.build(title: "Shirt")
When I try to call #item.order, I get an error not found, I guess it's because it's still unsaved to the DB, but how can make it point to the built object without saving any of the objects?
#item.order # Not Found - Tries to fetch from the DB
To make this work you have to tell the order association of your item model that it is the inverse of your items association or you order model and vice versa:
Like this:
class Item < ActiveRecord::Base
belongs_to :order, inverse_of: :items
end
class Order < ActiveRecord::Base
has_many :items, inverse_of: :order
end
That way your associations will be set up correctly even before saving to the database.
EDIT: If you don't want this you can always assign the association explicitly:
#item = #order.items.build(title: "Shirt")
#item.order = #order
or in one line:
#item = #order.items.build(title: "Shirt", order: #order)
I tested this in Rails 4.0.1 and it looks like using inverse_of is no longer needed as the associations are inferred properly. It was addressed in this commit: https://github.com/rails/rails/pull/9522
It is worth noting that there is still no solution for has_many through: relationships at this time.
That's because the #order haven't got an ID yet. So when calling #order.items.build the item has an nil in order_id field and thus cannot found the order you want.
I suggest you use the nested attributes feature to save some associated records together.

How can I design DB system while I want to change the ownership of records to the others when record is deleted?

Assuming I have these 4 models.
Then I'm using the gem called acts_as_paranoid for each model to implement logical deletion.
User
Community
Topic
Comment
User can resign anytime he wants. It means User's record will be deleted.
In general situation, communities, topics, and comments that are created by the user, should be also deleted together. (w/ dependant => destroy )
However, I don't want that. Because the other User might have added the community to his bookmark list. So for this reason they shouldn't be deleted.
When supposing that the user record was deleted but all those communities, topics and comments were remained, it starts returning nil error at the community page or wherever which was made by the user.
I'm coding like just this now.
It's gonna be nil everywhere since the user record is gone but all the records remain.
How can I handle this kind of problem?
views/communities/show.html.erb
<%= #community.user.username %>
What I want to do is, replacing the username displayed with this fixed word "Not Found User". Then possibly I'd just change the ownership(user_id) of community to the other User so that he can manage this community instead.
My association is just like this.
models/user.rb
has_many :communities
has_many :topics
has_many :comments
models/community.rb
belongs_to :user
has_many :topics
has_many :comments
models/topic.rb
belongs_to :user
belongs_to :community
has_many :comments
models/comment.rb
belongs_to :user
belongs_to :community
belongs_to :topic
I think the best way to handle this would be to refrain from deleting but put code in your display logic to handle a user that has been deleted. If you stick with acts_as_paranoid this would work fine, what I would do is use a helper method for username such as:
def community_username(community)
user = User.with_deleted.find(community.user_id)
if user.deleted_at.blank?
return user.username
end
"[deleted]"
end
You can put this in your appropriate helper or application helper and call it in your view like
<%= community_username(#community) %>
and it will display their username, or [deleted] if it has been deleted.
Note the above code is off the top of my head, you may need to adjust slightly if I'm forgetting acts_as_paranoids methods...

Accepts nested attributes convention for mongoid

I'm trying to create a form with nested attributes using mongoid. My models have the following code:
def Company
field :name
has_many :users, autosave: true, dependent: :destroy
accepts_nested_attributes_for :users
end
def User
belongs_to :company
has_one :profile
end
def Profile
belongs_to :user
end
The params that are returned from the form are in the following order:
"company"=>
{"users_attributes"=>
{"0"=>
{"profile_attributes"=>
{"first_name"=>"123123abcd123", "last_name"=>"abcd123123123"},
"email"=>"abcd#abcd.com123123123",
"password"=>"123123123123",
"password_confirmation"=>"123123123123"}},
"name"=>"abcd123123123",
"subdomain"=>"abcd123123123"}
Calling Company.create(params[:company]) seems to work, however it is not properly creating the user object. When I do company.users I can see that object, BUT when I do User.find, that document is not available. Reading the documentations I realized that the params should be passed in the following way:
"company"=>
{"users_attributes"=>
[{"profile_attributes"=>
{"first_name"=>"123123123", "last_name"=>"123123123"},
"email"=>"testin321#gmail.com",
"password"=>"123123",
"password_confirmation"=>"123123"}],
"name"=>"abcd123123123",
"subdomain"=>"abcd123123123"}
Note the subtle difference of using an array for users_attributes instead of a hash. This works right, but then it doesn't seem quite out of the box like it is with Active Record (and how it should be in something like in rails). I don't want to take the params hash and modify the data to make it follow certain conventions. Is there a better way, am I missing something?
if you can name the inputs as user_attributes[] that would make the array.
So instead of having user_attributes[0][profile_attributes] (I think you have something like this)
Make it to have user_attributes[][profile_attributes]
Could you post the code for the form? Then we can work on getting down to the reason it's formatted a certain way. (This should be a comment, but I am will to provide an answer once there are more details regarding the question.)
On a side note from your issue with the form in the view. I notice you are trying to create a company and it's nested users and the user and it's nested profile attributes as well. If you expect user to accept nested attributes for profile than you need to put that in the User model.
def User
belongs_to :company
has_one :profile, dependent: destroy, autosave: true
accepts_nested_attributes_for :profile
end
That may solve your problem, the error might arise from the User trying to mass-assign profile attributes without explicit instructions to do so.

Resources