audited icon indicating copy to clipboard operation
audited copied to clipboard

Deletion of joining records not being audited on Collection Associations

Open abhchand opened this issue 5 years ago • 3 comments

Hey audit gem maintainers 👋

Found a small issue that I'd love your feedback on. Also, really appreciate all the time the maintainers of this gem put into this great open source project.

Model Structure

Say I have the following model structure -

  • An Employee can have many Projects
  • A Project can have many Employees
  • Both models are associated to each other :through a joining table called EmployeeProject

The rails models look like this:

class Employee < ApplicationRecord
  audited

  has_many :employee_projects, dependent: :destroy
  has_many :projects, through: :employee_projects
  accepts_nested_attributes_for :projects, allow_destroy: true
end

class Project < ApplicationRecord
  audited

  has_many :employee_projects, dependent: :destroy
  has_many :employees, through: :employee_projects
  accepts_nested_attributes_for :employees, allow_destroy: true
end

class EmployeeProject < ApplicationRecord
  audited

  belongs_to :employee
  belongs_to :project

  validates :employee, presence: true
  validates :project, presence: true
end

The Issue

Rails provides collection association helpers on models that use the has_many :through association.

So in the above example rails automatically provides the following -

  • @employee.project_ids=
  • @project.employee_ids=

These helpers are absolute - they automatically add and remove any necessary joining table records based on the array you provide them. So @employe.project_ids = [1, 2] would ensure that only projects 1, and 2 are joined to this @employee record. If any other records exist, they will be destroyed when the model is saved. (EDIT: It actually calls save itself automatically, so changes take effect immediately)

The issue is that the Audit gem correctly audits any newly created joining records using the above helpers, but it does not correctly audit any destroyed joining records.

Example

Initialize models and note that there are no audit records

> @employee = Employee.first
> @project = Project.first

> Audited::Audit.count   # => 0

Associate @employee and @project using the project_ids= collection association helper.

This correctly creates an audit record for the new EmployeeProject joining record

> @employee.project_ids = [@project.id]  # => [15]
> @employee.save                         # => true

> Audited::Audit.count                   # => 1
> Audited::Audit.first.auditable_type    # => "EmployeeProject"

Now disassociate @employee and @project which will delete the EmployeeProject joining record.

This should create another Audit record for the deletion of the joining record... but it doesn't

> @employee.project_ids = []             # => []
> @employee.save                         # => true

> Audited::Audit.count                   # => 1

abhchand avatar Sep 06 '20 00:09 abhchand

I had the same problem and found a solution.

For you, adding "dependent: :destroy" to your "has_many through" statement should make it work:

class Employee < ApplicationRecord
    has_many :projects, through: :employee_projects, dependent: :destroy
end

Note that destroying the the employee does not destroy the projects. This is more of a Rails oddity.

Also, it's a bit weird you have audited on all three models. I would suggest changing it to:

class Employee < ApplicationRecord
  audited  ## Assuming Employee is the main model where you will compile "own_and_associated_audits" data
  has_associated_audits

  has_many :employee_projects, dependent: :destroy
  has_many :projects, through: :employee_projects, dependent: :destroy  ## <== CHANGED
  accepts_nested_attributes_for :projects, allow_destroy: true
end

class Project < ApplicationRecord
   ## <== CHANGED, removed audited unless you need it from this model

  has_many :employee_projects, dependent: :destroy
  has_many :employees, through: :employee_projects
  accepts_nested_attributes_for :employees, allow_destroy: true
end

class EmployeeProject < ApplicationRecord
  audited associated_with: :employee  ## <== CHANGED

  belongs_to :employee
  belongs_to :project

  validates :employee, presence: true
  validates :project, presence: true
end

chase439 avatar Nov 18 '20 21:11 chase439

Hi @chase439 and @abhchand ... adding dependent: :destroy didn't solve this for me. I can see the SQL log entry for deleting the entry in the joining table but no "destroy" audit is created in that case.

Any further hints?

THX, mscdit

mscdit avatar Apr 21 '24 12:04 mscdit