How to fetch previous values of associated records? - ruby-on-rails

When working with many-to-many relationships, I need to maintain a log file recording the changed values.
Using the before_save and after_save callbacks works fine for the main (has_many) model itself but in the before_save callback the associated (belongs_to) records appear to be updated already!
It seems rather odd to see that some parts of the data already have been updated before the 'before_save' callback is called.
Also, using callbacks in the associated model reveals that there is no before_destroy executed. Only the before_save gets called and shows the new values.
I also tried the :prepend => :true option but that didn't gave other results.
When turning on SQL logging before the actual save in the main (has_many) model, I can see Rails is fetching the associated records, determines the differences and deletes the surplus record(s). The before_destroy of the associated model is not called.
It then calls the before_save of the associated model and inserts the new ones (if any) and commits the transaction. This is all done JUST BEFORE the before_save of the main model.
Does anyone know how to fetch the associated records before they are changed?
I would expect the before_destroy of the associated model would get called and let me handle it.

Use before_update and you can access old values using _was
before_update :record_values
def record_values
p "oldvalue: #{self.field_was} Newvalue: #{self.field}"
end

Your question is a bit unclear, but let me provide you with an example of what I think you try to do.
The code below works fine for me:
class Person < ApplicationRecord
has_many :addresses
validates_presence_of :name
before_save { puts "before_save of person - changes: #{changes}" }
before_destroy { puts "before_destroy of person with id: #{id}" }
end
class Address < ApplicationRecord
belongs_to :person, required: true
validates_presence_of :name
before_save { puts "before_save of address - changes: #{changes}" }
before_destroy { puts "before_destroy of address with id: #{id}" }
end
This results in the following output when interacting:
person = Person.create(name: 'Johan Wentholt')
# before_save of person - changes: {"name" =>[nil, "Johan Wentholt"]}
#=> #<Person id: 2, name: "Johan Wentholt", created_at: "2017-10-25 15:04:27", updated_at: "2017-10-25 15:04:27">
person.addresses.create(name: 'Address #1')
# before_save of address - changes: {"person_id"=>[nil, 2], "name" =>[nil, "Address #1"]}
#=> #<Address id: 7, person_id: 2, name: "Address #1", created_at: "2017-10-25 15:06:38", updated_at: "2017-10-25 15:06:38">
person.addresses.last.update(name: 'Address without typo')
# before_save of address - changes: {"name"=>["Address #1", "Address without typo"]}
#=> true
person.update(name: 'Kaasboer')
# before_save of person - changes: {"name"=>["Johan Wentholt", "Kaasboer"]}
#=> true
person.addresses.last.destroy
# before_destroy of address with id: 7
#=> #<Address id: 7, person_id: 2, name: "Address without typo", created_at: "2017-10-25 15:06:38", updated_at: "2017-10-25 15:08:51">
person.destroy
# before_destroy of person with id: 2
#=> #<Person id: 2, name: "Kaasboer", created_at: "2017-10-25 15:04:27", updated_at: "2017-10-25 15:10:46">
As you can see this logs all changes. Like I said, the question is a bit unclear, but I hope this helps you further.
Keep in mind that some Rails methods don't trigger callbacks. For example: delete, update_all, update_column and some others.
For more about changes take a look at: ActiveModel::Dirty

For clarity sake, lets give some extended information:
class Book < ActiveRecord::Base
unloadable
has_many :titles, dependent: :destroy
has_many :authors, :through => :titles
accepts_nested_attributes_for :authors
before_save :pre_save
after_save :post_save
before_destroy :pre_delete
def pre_save
#nr = self.new_record?
end
def pre_save
changed_values = []
if #nr
changed_values.push "New record created"
else
self.changes.each do |field, cvs|
changes.push("#{field} : #{cvs[0]} => #{cvs[1]}")
end
end
if changes.length > 0
BookLog.create(:book_id => self.id, :changed_values => changes.join(', '))
end
end
def pre_delete
BookLog.create(:book_id => self.id, :changed_values => "Deleted: #{self.name}")
end
end
class Title < ActiveRecord::Base
unloadable
belongs_to :book
belongs_to :author
end
class Author < ActiveRecord::Base
unloadable
has_many :titles, dependent: :destroy
has_many :books, :through => :titles
accepts_nested_attributes_for :books
end
class BooksController < ApplicationController
def edit
book = Book.find(params[:book][:id])
book.name = .....
===> Here the old values are still available <====
book.author_ids = params[:book][:author_ids]
===> Now the new values are written to the database! <====
book.save!
end
end
Changes to the Book record are perfectly logged.
But there is no way to fetch the changed associated values for author_ids.
A before_destroy callback in Title was not called, the after_save was.
I checked this with enabling the SQL logging just before the assignment of the new author_ids to the edited record. I could see that Rails determines the differences between the existing and new associated values, deletes the surplus form the Titles table and insert the extra ones (if any)
I solved it by moving the logging for the changes in Titles to the Books controller by comparing the old with the new values:
o_authors = book.author_ids
n_authors = params[:book][:author_ids].collect {|c| c.to_i}
diff = o_authors - n_authors | n_authors - o_authors
if !diff.empty?
changed_values = []
(o_authors - n_authors).each do |d|
changed_values.push("Removed Author: #{Author.find(d).name}")
end
(n_authors - o_authors).each do |d|
changed_values.push("Added Author: #{Author.find(d).name}")
end
BookLog.create(:book_id => book.id, :changed_values => changed_values)
end
book.author_ids = params[:book][:author_ids]
book.save!
Like I said, it works but IMHO it does not reflect the Rails way of doing things. I would have expected to get the previous author_ids in the same way as any other Book attribute.

Related

Rails ActiveRecord: Can you auto-create a belongs_to association without defining the corresponding has_many association?

I essentially have multiple models with a belongs_to association to another model, but I don't want to define all the has_many relationships on the parent model. Is there a way to create and link this association in one call?
class Thing < ActiveRecord::Base
# does not have any has_many or has_one associations defined
end
class Comment < ActiveRecord::Base
belongs_to :thing
end
# in a controller...
thing = #comment.build_thing(thing_params)
if thing.save
# thing was created but #comment.thing_id was not updated
end
if thing.save && #comment.update(thing: thing)
# this works, but requires an extra call to update the comment
else
# now we would have to check which model failed to save
end
Is there a simple way to do this that I am missing?
has_many association wouldn't change anything (unless :inverse_of is set), you have to save comment either way:
# i assume `belongs_to :thing` is optional v
comment = Comment.create #=> #<Comment:0x00007fec1b612088 id: 1, thing_id: nil>
comment.build_thing #=> #<Thing:0x00007ff8ba420788 id: nil>
comment.save
comment.thing_id #=> 1
comment.thing #=> #<Thing:0x00007ff8ba420788 id: 1>
or like this:
Comment.create(thing: Thing.new) #=> #<Comment:0x00007ff8ba35c1d0 id: 2, thing_id: 2>

Rails - Object in has_many relation is not getting updating

I am having a table called Groups, where Groups and Sub-Groups are saved. A group has_many sub-group. Below is the code:
groupone.rb (one of the main group)
class GroupOne < BaseGroup
belongs_to :parent, class_name: 'GroupOne'
has_many :sub_group_one, autosave: true, dependent: :destroy, inverse_of: :groupone
end
reports_controller.rb
class ReportsController < ActionController::Base
def process_report
current_record = load_from_xml(xml_path)
current_group = current_record.last
base_report_group = find_or_create_base_group(current_group)
process_sub_group(current_group, base_report_group)
base_report_group.save
end
def process_sub_group(current_group, base_report_group)
if current_group.sub_group_one.present?
current_group.sub_group_one.each do |sub_group|
sgroup = base_report_group.sub_group_one.find_or_initialize_by(group_type_id: sub_group.group_type, serial_num: sub_group.serial_num)
sgroup.attributes = {name: "Rob", age: 12}
end
end
end
end
The above code is creating new GroupOne and many sub_group_one records with out any issues but when I try to update the existing sub_group_one values, they are not getting updated. For example {name: "Rob", age: 12} is not getting updated to any of the sub-group record. I noticed that the new attributes are assigned to sgroup during the current sub_group iteration and once all the sub_group iteration are completed, when I do binding.pry for base_report_group.sub_group_one it shows the old record and this is the issue.
Can any one please help me to fix this?

Rails: why is calling `valid?` only validate some of associated record not all associated records

I have a model Order which is like
# app/models/order.rb
class Order< ApplicationRecord
has_one :detail
has_one :extra
..
end
I have two orders
order1 = Order.first
order1.detail #<OrderDetail:0x00 name: "abc", remark: 'test1'>
order1.extra #<OrderExtra:0x00 email: nil, recipent: nil>
order2 = Order.second
order1.detail #<OrderDetail:0x00 name: "abc", remark: 'test1'>
order1.extra #<OrderExtra:0x00 email: nil, recipent: "xyz">
When I call order1.valid? or order1.save! it will not check OrderExtra validation and returns true. But when I call order2.valid? or order2.save! it checks OrderExtra validation.
order1.save! # true
order2.save! # ActiveRecord Invalid OrderExtra
I want to know how rails checks if they want to check associated validation when call save! and the reason behind that.
Please let me know if any additional requirement needed on this.
use the validates_associated for enforcing associated model validations
class Book < ActiveRecord::Base
has_many :pages
belongs_to :library
validates_associated :pages, :library
end
This validation will not fail if the association hasn’t been assigned. If you want to ensure that the association is both present and guaranteed to be valid, you also need to use validates_presence_of.
class Library < ActiveRecord::Base
has_many :books
validates_presence_of :name
end

Update attributes on has_many through associations and working with the unsaved object

This has something to do with my last quesion about unsaved objects, but now it is more about a specific problem how to use rails.
The models I have are:
class User < ActiveRecord::Base
has_many :project_participations
has_many :projects, through: :project_participations, inverse_of: :users
end
class ProjectParticipation < ActiveRecord::Base
belongs_to :user
belongs_to :project
enum role: { member: 0, manager: 1 }
end
class Project < ActiveRecord::Base
has_many :project_participations
has_many :users, through: :project_participations, inverse_of: :projects
accepts_nested_attributes_for :project_participations
end
With this models, when I create a new project I can do it by a form (fields_for etc) and then I can call update_attributes in the controller. So if I have users in the database already, I can do this:
u = Users.create # save one user in database (so we have at least one saved user)
p = Project.new
# add the user to the project as a manager
# the attributes could come from a form with `.fields_for :project_participations`
p.update_attributes(project_participations_attributes: [{user_id: u.id, role: 1}])
=> true
This works fine until I want to do something with the users of a project. For example I want add a validations that there must be at least one user for a project:
class Project < ActiveRecord::Base
...
validates :users, presence: true # there must be at least one user in a project
...
end
This now gives:
u = Users.create
p = Project.new
p.update_attributes(project_participations_attributes: [{user_id: u.id, role: 1}])
=> false
p.errors
=> #<ActiveModel::Errors:... #base=#<Project id: nil>, #messages={:users=>["can't be blank"]}>
p.users
=> #<ActiveRecord::Associations::CollectionProxy []>
p.project_participations
=> #<ActiveRecord::Associations::CollectionProxy [#<ProjectParticipation id: nil, user_id: 1, project_id: nil>]>
So on unsaved projects the .users is empty. This already bugs me (see my last quesion about unsaved objects). But in this case I can of course now work around this by doing validates :project_participations, presence: true instead of validates :users, presence: true and it should mean the same.
But this would mean I should never use the .users method (in any helper, model, view, ...) unless I am totally sure that I work with a saved object. Which in fact renders the .users method unusable (like it does with the validation of user`s presence).
If I call update_attributes like this, the validations works and it saves:
p.update_attributes(users: [u])
With this it creates the project_participation by itself so p.users works as expected. But here I cannot set any data like role for project_participation of that user.
So my questions are: Can I make the .users method work whether or not the object is saved (I think not)? But then, how can I add users to a unsaved project as a manager/member and work with the unsaved project?
I hope my problem is clear.
I think I understand you question, and you're correct in assuming that you cannot use the .users method whether or not the project model is saved. The reason for this is that in defining an association in Project (ie. has_many :users, through: :project_participations, inverse_of: :projects) you're telling rails to read the users attribute out of the database via the project_participations join table and when you haven't saved the project you have nothing to read out of the database.
In order to add a User to your project in a particular role you will need to create a new ProjectParticipation model which you will then associate to your project. If you then remove the users association and write your own users method you should be able to access your collection of users regardless of whether or not the project has been saved.
class Project < ActiveRecord::Base
has_many :project_participations
...
def users
project_participations.collect { |pp| pp.user }
end
end
Then something like:
u = Users.create
p = Project.new
pp = ProjectParticipation.new({user: u, project: p, role: 1})
p.project_participations << pp
p.users
Hopefully that helps.

Creating associated models on create in Rails?

I have to be dead tired because I really can't figure out such simple task as this.
Having:
class Account < ActiveRecord::Base
has_one :subscription, :dependent => :destroy
after_save :append_subscription
private
def append_subscription
# TODO
end
end
# Subscription(id: integer, account_id: integer, level: integer (: 1), starts_at: date, ends_at:date, created_at: datetime, updated_at: datetime)
class Subscription < ActiveRecord::Base
belongs_to :account
end
I'm trying resolve the TODO part, or am I going about it the wrong way? Here's the test.
describe Account do
include AccountSpecHelper
it "should have a subscription at least at level one on creation" do
account = Account.create
account.subscription.level.should be(1)
end
end
Why after_save and not before_create and let ActiveRecord worry about creating associated model and assigning account_id correctly?
I haven't checked, but this should work:
class Account
before_create {|account| account.build_subscription(params)} # or move it to method
end

Resources