Belongs_to without raising an exception if association does not exist

Is it possible in Rails to use belongs_to but ignore / do not raise when the association no longer exists (e.g. invalid referenced ID).

This would be useful for example in an audit log. The log record still exists, but the object referenced may no longer exist.

Example:

log_entry.user # the User object or nil if the user_id no longer exists (e.g. account deleted)

Check out the dependent: :nullify option on the has_many relationship. Your models would be:

class User < ApplicationRecord
  has_many :log_entries, dependent: :nullify
end

class LogEntry < ApplicationRecord
  belongs_to :user, optional: true
end

This will cause the user_id of all related log entries to be set to NULL when a user is destroyed. This is better than just ignoring the error as it ensures your database maintains referential integrity.

Note, you need the optional: true on the belongs_to relationship since belongs_to assumes a presence validation. Also make sure your database allows a NULL value for the user_id in your log_entries table. This is the default when you create a belongs_to field in a migration but if you previously had null: false in your migration you can change_column_null change that with:

change_column_null :log_entries, :user_id, true

Thanks for the suggestion! I already know about that solution, but I prefer not to use it in this specific case.

The “log” table is too large and deleting a user could produce literally millions of updates… It’s much better to consider logs as immutable records. They are deleted in any case after some time (e.g. 30 or 90 days).

For this reason I was wondering if there is something like belongs_to :user, ignore_not_found: true

In any case i understand that maybe my requirement is very specific.

For now, as a workaround, I simply use a user_id field directly, without the belongs_to

Isn’t belongs_to :user, optional: true all you need here? It depends how you define “ignore”, but it would have the user be nil if it’s not found which sounds like what you want.

For this reason I was wondering if there is something like belongs_to :user, ignore_not_found: true

Gotcha, for performance reasons you want to leave the dangling reference in the log_entries table but if someone does call the user method it won’t error trying to lookup that non-existent user. I actually think it does currently work like that. Below is a quick test script I did to verify:

require 'bundler/inline'

gemfile do
 source 'https://rubygems.org'
 gem 'activerecord', require: 'active_record'
 gem 'sqlite3'
 gem 'minitest'
 gem 'minitest-bang', require: 'minitest/bang'
end

require 'minitest/spec'
require 'minitest/autorun'

describe 'belongs_to without referential integrity' do
  let!(:user) { User.create! }
  let!(:log_entry) { LogEntry.create! user: }

  before { user.destroy }

  it 'does not error with dangling reference' do
    expect( log_entry.reload.user ).must_be_nil
  end
end

ActiveRecord::Base.establish_connection adapter: 'sqlite3', database: ':memory:'
ActiveRecord::Migration.verbose = false

ActiveRecord::Schema.define version: 1 do
  create_table :users
  create_table :log_entries do |t|
    t.belongs_to :user
  end
end

class User < ActiveRecord::Base
  has_many :log_entries
end

class LogEntry < ActiveRecord::Base
  belongs_to :user
end

In the above I did not put dependent: :nullify on the user model or the optional: true on the LogEntry model. I delete the user (so we have a dangling reference in the database) and then try to access the user via the log entry. The test passes not raising an error.

I believe this happens because Rails rescues the ActiveRecord::RecordNotFound exception and just resets the association silently ignoring the error.

Thank you very much for all this relevant information.

My confusion came from this sentence in the API docs:

Post#author (similar to Author.find(author_id))

ActiveRecord will skip database queries if you set the id to null, which means it’ll be more performant to not have corrupted referential integrity.

But, is there any reason you can’t just anonymize the account and not actually delete the User record? Then you’ll know that user X did some things (which can be relevant for auditing / accounting purposes) but not know PII about that account anymore.