I know that in Rails 5 before_destroy
callback does not take into account false
return value but throw(:abort)
instead. You can find more about it here and there and in Rails API docs.
But prepend
option still does not work in the following situation.
I need to prevent the deletion of a last record in the following model:
class ShopLanguage < ApplicationRecord
belongs_to :shop
belongs_to :language
has_many :descriptions, inverse_of: :shop_language, dependent: :destroy
validates :language, :shop, presence: true
validates :language, uniqueness: { scope: :shop, message: "already defined for this shop"}
before_destroy :check_last_language, prepend: true
private
def check_last_language
if shop.languages.count < 2
errors[:base] << "You can't delete the last language"
false
end
end
end
Here is Shop model:
class Shop < ApplicationRecord
has_many :users
has_many :links, inverse_of: :shop
has_many :shop_languages, inverse_of: :shop, dependent: :destroy
has_many :languages, through: :shop_languages
...
Even if I move the callback upper just before declaring the relations:
class ShopLanguage < ApplicationRecord
before_destroy :check_last_language, prepend: true
...
it does not work either. Here is what I tried in a rails console:
shop = Shop.first
#<Shop id: 2, category: "Sport", fax: nil, identifier: 1, leader: "XXXXX", modified_by: "batch.update.shops", name: "ENGLOS", opening_date: "1976-07-27", phone: "0320001700", status: "open", created_at: "2018-03-22 16:39:35", updated_at: "2018-03-22 16:39:35">
2.4.0 :002 > lang = Language.find(245)
Language Load (0.4ms) SELECT "languages".* FROM "languages" WHERE "languages"."id" = $1 LIMIT $2 [["id", 245], ["LIMIT", 1]]
=> #<Language id: 245, tag: "fr-FR", created_at: "2018-03-22 16:25:59", updated_at: "2018-03-22 16:25:59">
shop.languages
Language Load (1.0ms) SELECT "languages".* FROM "languages" INNER JOIN "shop_languages" ON "languages"."id" = "shop_languages"."language_id" WHERE "shop_languages"."shop_id" = $1 LIMIT $2 [["shop_id", 2], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy [#<Language id: 245, tag: "fr-FR", created_at: "2018-03-22 16:25:59", updated_at: "2018-03-22 16:25:59">]>
shop.languages.delete(lang)
(0.4ms) SAVEPOINT active_record_1
SQL (0.5ms) DELETE FROM "shop_languages" WHERE "shop_languages"."shop_id" = $1 AND "shop_languages"."language_id" = 245 [["shop_id", 2]]
(0.2ms) RELEASE SAVEPOINT active_record_1
=> [#<Language id: 245, tag: "fr-FR", created_at: "2018-03-22 16:25:59", updated_at: "2018-03-22 16:25:59">]
2.4.0 :007 > shop.languages.size
(3.5ms) SELECT COUNT(*) FROM "languages" INNER JOIN "shop_languages" ON "languages"."id" = "shop_languages"."language_id" WHERE "shop_languages"."shop_id" = $1 [["shop_id", 2]]
=> 0
So, the callback hasn't been called. The problem is that, as explained in Rails Guides,
5 Skipping Callbacks Just as with validations, it is also possible to skip callbacks by using the following methods:
decrement decrement_counter delete delete_all
So, if I got it right, when I called delete
on my collection:
shop.languages.delete(lang)
my callback method will never be called. More of that, Active Record or Active Model callbacks no longer halt when returning false, so the only solution would be to use:
throw(:abort) if shop.languages.count < 2
The question is how to delete properly a record from the table shop_languages
without deleting the corresponding language record in the table languages
? Using
throw(:abort) if ...
seems not to be enough readable and convenient way for an API response.