Rails 6 adds delete_by, destroy_by ActiveRecord::Relation

Rails 6 added delete_by and destroy_by methods to ActiveRecord::Relation, providing convenient ways to find and delete/destroy records in a single method call.

Before Rails 6

Previously, you had to find records first, then delete them:

# Before: Two-step process
users = User.where(active: false)
users.each(&:destroy)
# or
User.where(active: false).destroy_all

Rails 6 Solution

Rails 6 provides delete_by and destroy_by for single-record operations:

# Delete a single record
User.delete_by(email: 'inactive@example.com')

# Destroy a single record
User.destroy_by(email: 'inactive@example.com')

delete_by

The delete_by method finds and deletes a record without callbacks:

# Delete without callbacks
User.delete_by(email: 'old@example.com')

# Returns the deleted record or nil
deleted_user = User.delete_by(id: 123)

Characteristics

  • No callbacks: Skips before_destroy, after_destroy, etc.
  • Faster: Direct SQL DELETE
  • No validations: Doesn't run validations
  • Returns record: Returns the deleted record or nil

destroy_by

The destroy_by method finds and destroys a record with callbacks:

# Destroy with callbacks
User.destroy_by(email: 'old@example.com')

# Returns the destroyed record or nil
destroyed_user = User.destroy_by(id: 123)

Characteristics

  • Runs callbacks: Executes before_destroy, after_destroy, etc.
  • Validations: Runs validations
  • Slower: More overhead due to callbacks
  • Returns record: Returns the destroyed record or nil

Basic Usage

Single Condition

# Delete by email
User.delete_by(email: 'inactive@example.com')

# Destroy by ID
User.destroy_by(id: 123)

Multiple Conditions

# Delete with multiple conditions
User.delete_by(active: false, role: 'guest')

# Destroy with multiple conditions
User.destroy_by(active: false, role: 'guest')

Real-World Examples

Cleaning Up Inactive Users

# Delete inactive users older than 1 year
User.delete_by(
  active: false,
  'created_at < ?': 1.year.ago
)

Removing Expired Sessions

# Destroy expired sessions
Session.destroy_by('expires_at < ?', Time.current)

Cleaning Up Orphaned Records

# Delete comments without a post
Comment.delete_by(post_id: nil)

# Or with joins
Comment.left_joins(:post)
  .where(posts: { id: nil })
  .delete_by(id: Comment.arel_table[:id])

Comparison with Other Methods

vs find_by + destroy

# Old way
user = User.find_by(email: 'old@example.com')
user&.destroy

# New way
User.destroy_by(email: 'old@example.com')

vs where + destroy_all

# For single record
User.where(email: 'old@example.com').destroy_all

# More explicit with delete_by/destroy_by
User.destroy_by(email: 'old@example.com')

vs delete_all

# delete_all works on relations, not single records
User.where(active: false).delete_all

# delete_by works on single record
User.delete_by(active: false)

Return Values

Both methods return the deleted/destroyed record or nil:

# Returns the record if found
user = User.delete_by(id: 123)
# => #<User id: 123, ...>

# Returns nil if not found
user = User.delete_by(id: 999999)
# => nil

# Check if deletion was successful
if User.delete_by(email: 'old@example.com')
  puts "User deleted"
else
  puts "User not found"
end

Use Cases

Conditional Deletion

# Only delete if certain conditions are met
if user.inactive_for_30_days?
  User.delete_by(id: user.id)
end

Safe Deletion

# Use destroy_by when you need callbacks
User.destroy_by(id: user.id) # Runs callbacks, validations

# Use delete_by for performance
User.delete_by(id: user.id) # Faster, no callbacks

Batch Operations

# Delete multiple records (one at a time)
emails = ['old1@example.com', 'old2@example.com']
emails.each { |email| User.delete_by(email: email) }

Performance Considerations

delete_by

  • Faster: Direct SQL DELETE
  • No callbacks: Skips ActiveRecord overhead
  • Use when: You don't need callbacks

destroy_by

  • Slower: Runs callbacks and validations
  • More overhead: Full ActiveRecord lifecycle
  • Use when: You need callbacks (e.g., dependent: :destroy)

Best Practices

  1. Use delete_by for performance: When callbacks aren't needed
  2. Use destroy_by for safety: When you need callbacks and validations
  3. Handle return values: Check if deletion was successful
  4. Be careful with conditions: Ensure conditions match exactly one record

Common Patterns

Safe Deletion

def delete_inactive_user(email)
  user = User.delete_by(email: email, active: false)
  if user
    Rails.logger.info("Deleted inactive user: #{email}")
  else
    Rails.logger.warn("User not found or active: #{email}")
  end
  user
end

Conditional Destruction

def cleanup_old_records
  old_records = Post.where('created_at < ?', 1.year.ago)
  old_records.find_each do |post|
    Post.destroy_by(id: post.id) if post.comments.empty?
  end
end

Limitations

  • Only deletes/destroys one record (the first match)
  • For multiple records, use delete_all or destroy_all
  • Conditions should ideally match a single record

Conclusion

Rails 6's delete_by and destroy_by methods provide convenient ways to find and delete/destroy single records. delete_by is faster and skips callbacks, while destroy_by runs the full ActiveRecord lifecycle. Choose the appropriate method based on whether you need callbacks and validations.

Rails 6 adds delete_by, destroy_by ActiveRecord::Relation - Abhay Nikam