Favoring dependent: :delete_all over :destroy (#15442)

* Favoring dependent: :delete_all over :destroy

The `dependent: :destroy` callback is slower than `dependent: :delete`
and `dependent: :delete_all`.  We need only favor the `dependent:
:destroy` when there's callbacks that happen.

In the case of :destroy, ActiveRecord instantiates each object and then
runs destroy.  Whereas in the case of :delete, ActiveRecord issues a SQL
command to delete the related files.

It is often "safer" to use :destroy, as it guarantees that you'll
instantiate the record and run it's callbacks.  But sometimes you have
to go with the speed of SQL.

This relates to #14140.  Note there is still more to consider, but given
that we're looking at moving from :destroy on all of an article's page
views to :delete, we might buy enough time in the callback.

* Removing redundancy of article destroy

Prior to this commit, `before_destroy_actions` called the `bust_cache`
method which in turn called `touch_actor_latest_article_updated_at` but
`bust_cache` did not pass the destroying parameter.  Then
`before_destroy_actions` immediately called
`touch_actor_latest_article_updated_at` with `destroying: true`.

With this change, we remove one of those duplicate calls.

* Fixing specs regarding relationships

* Fixing specs regarding relationships
This commit is contained in:
Jeremy Friesen 2021-11-24 13:56:16 -05:00 committed by GitHub
parent 9df5f6af49
commit be97f2fc07
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 52 additions and 20 deletions

View file

@ -32,17 +32,21 @@ class Article < ApplicationRecord
UNIQUE_URL_ERROR = "has already been taken. " \
"Email #{ForemInstance.email} for further details.".freeze
has_one :discussion_lock, dependent: :destroy
has_one :discussion_lock, dependent: :delete
has_many :mentions, as: :mentionable, inverse_of: :mentionable, dependent: :destroy
has_many :mentions, as: :mentionable, inverse_of: :mentionable, dependent: :delete_all
has_many :comments, as: :commentable, inverse_of: :commentable, dependent: :nullify
has_many :html_variant_successes, dependent: :nullify
has_many :html_variant_trials, dependent: :nullify
has_many :notification_subscriptions, as: :notifiable, inverse_of: :notifiable, dependent: :destroy
has_many :notification_subscriptions, as: :notifiable, inverse_of: :notifiable, dependent: :delete_all
has_many :notifications, as: :notifiable, inverse_of: :notifiable, dependent: :delete_all
has_many :page_views, dependent: :destroy
has_many :page_views, dependent: :delete_all
# `dependent: :destroy` because in Poll we cascade the deletes of
# the poll votes, options, and skips.
has_many :polls, dependent: :destroy
has_many :profile_pins, as: :pinnable, inverse_of: :pinnable, dependent: :destroy
has_many :profile_pins, as: :pinnable, inverse_of: :pinnable, dependent: :delete_all
# `dependent: :destroy` because in RatingVote we're relying on
# counter_culture to do some additional tallies
has_many :rating_votes, dependent: :destroy
has_many :top_comments,
lambda {
@ -585,8 +589,7 @@ class Article < ApplicationRecord
end
def before_destroy_actions
bust_cache
touch_actor_latest_article_updated_at(destroying: true)
bust_cache(destroying: true)
article_ids = user.article_ids.dup
if organization
organization.touch(:last_article_at)
@ -794,13 +797,13 @@ class Article < ApplicationRecord
organization&.touch(:latest_article_updated_at)
end
def bust_cache
def bust_cache(destroying: false)
cache_bust = EdgeCache::Bust.new
cache_bust.call(path)
cache_bust.call("#{path}?i=i")
cache_bust.call("#{path}?preview=#{password}")
async_bust
touch_actor_latest_article_updated_at
touch_actor_latest_article_updated_at(destroying: destroying)
end
def calculate_base_scores

View file

@ -1,6 +1,10 @@
# @note When we destroy the related user, it's using dependent:
# :delete for the relationship. That means no before/after
# destroy callbacks will be called on this object.
#
# @note When we destroy the related article, it's using dependent:
# :delete for the relationship. That means no before/after
# destroy callbacks will be called on this object.
class DiscussionLock < ApplicationRecord
belongs_to :article
belongs_to :locking_user, class_name: "User"

View file

@ -1,6 +1,10 @@
# @note When we destroy the related user, it's using dependent:
# :delete for the relationship. That means no before/after
# destroy callbacks will be called on this object.
#
# @note When we destroy the related article, it's using dependent:
# :delete for the relationship. That means no before/after
# destroy callbacks will be called on this object.
class Mention < ApplicationRecord
belongs_to :user
belongs_to :mentionable, polymorphic: true

View file

@ -1,6 +1,10 @@
# @note When we destroy the related user, it's using dependent:
# :delete for the relationship. That means no before/after
# destroy callbacks will be called on this object.
#
# @note When we destroy the related article, it's using dependent:
# :delete for the relationship. That means no before/after
# destroy callbacks will be called on this object.
class NotificationSubscription < ApplicationRecord
belongs_to :notifiable, polymorphic: true
belongs_to :user

View file

@ -1,3 +1,6 @@
# @note When we destroy the related article, it's using dependent:
# :delete for the relationship. That means no before/after
# destroy callbacks will be called on this object.
class PageView < ApplicationRecord
belongs_to :user, optional: true
belongs_to :article

View file

@ -5,9 +5,9 @@ class Poll < ApplicationRecord
belongs_to :article
has_many :poll_options, dependent: :destroy
has_many :poll_skips, dependent: :destroy
has_many :poll_votes, dependent: :destroy
has_many :poll_options, dependent: :delete_all
has_many :poll_skips, dependent: :delete_all
has_many :poll_votes, dependent: :delete_all
validates :poll_options_count, presence: true
validates :poll_options_input_array, presence: true, length: { minimum: 2, maximum: 15 }

View file

@ -1,3 +1,6 @@
# @note When we destroy the related poll, it's using dependent:
# :delete for the relationship. That means no before/after
# destroy callbacks will be called on this object.
class PollOption < ApplicationRecord
belongs_to :poll
has_many :poll_votes, dependent: :destroy

View file

@ -1,6 +1,10 @@
# @note When we destroy the related user, it's using dependent:
# :delete for the relationship. That means no before/after
# destroy callbacks will be called on this object.
#
# @note When we destroy the related poll, it's using dependent:
# :delete for the relationship. That means no before/after
# destroy callbacks will be called on this object.
class PollSkip < ApplicationRecord
belongs_to :poll
belongs_to :user

View file

@ -1,6 +1,10 @@
# @note When we destroy the related user, it's using dependent:
# :delete for the relationship. That means no before/after
# destroy callbacks will be called on this object.
#
# @note When we destroy the related poll, it's using dependent:
# :delete for the relationship. That means no before/after
# destroy callbacks will be called on this object.
class PollVote < ApplicationRecord
belongs_to :user
belongs_to :poll_option

View file

@ -1,3 +1,6 @@
# @note When we destroy the related article (via pinnable), it's using
# dependent: :delete for the relationship. That means no
# before/after destroy callbacks will be called on this object.
class ProfilePin < ApplicationRecord
belongs_to :pinnable, polymorphic: true
belongs_to :profile, polymorphic: true

View file

@ -18,17 +18,17 @@ RSpec.describe Article, type: :model do
it { is_expected.to belong_to(:organization).optional }
it { is_expected.to belong_to(:user) }
it { is_expected.to have_one(:discussion_lock).dependent(:destroy) }
it { is_expected.to have_one(:discussion_lock).dependent(:delete) }
it { is_expected.to have_many(:comments).dependent(:nullify) }
it { is_expected.to have_many(:mentions).dependent(:destroy) }
it { is_expected.to have_many(:mentions).dependent(:delete_all) }
it { is_expected.to have_many(:html_variant_successes).dependent(:nullify) }
it { is_expected.to have_many(:html_variant_trials).dependent(:nullify) }
it { is_expected.to have_many(:notification_subscriptions).dependent(:destroy) }
it { is_expected.to have_many(:notification_subscriptions).dependent(:delete_all) }
it { is_expected.to have_many(:notifications).dependent(:delete_all) }
it { is_expected.to have_many(:page_views).dependent(:destroy) }
it { is_expected.to have_many(:page_views).dependent(:delete_all) }
it { is_expected.to have_many(:polls).dependent(:destroy) }
it { is_expected.to have_many(:profile_pins).dependent(:destroy) }
it { is_expected.to have_many(:profile_pins).dependent(:delete_all) }
it { is_expected.to have_many(:rating_votes).dependent(:destroy) }
it { is_expected.to have_many(:sourced_subscribers) }
it { is_expected.to have_many(:reactions).dependent(:destroy) }

View file

@ -9,9 +9,9 @@ RSpec.describe Poll, type: :model do
describe "builtin validations" do
subject { poll }
it { is_expected.to have_many(:poll_options).dependent(:destroy) }
it { is_expected.to have_many(:poll_skips).dependent(:destroy) }
it { is_expected.to have_many(:poll_votes).dependent(:destroy) }
it { is_expected.to have_many(:poll_options).dependent(:delete_all) }
it { is_expected.to have_many(:poll_skips).dependent(:delete_all) }
it { is_expected.to have_many(:poll_votes).dependent(:delete_all) }
it { is_expected.to validate_length_of(:poll_options_input_array).is_at_least(2).is_at_most(15) }