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
- Use delete_by for performance: When callbacks aren't needed
- Use destroy_by for safety: When you need callbacks and validations
- Handle return values: Check if deletion was successful
- 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_allordestroy_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.