Rails doesn't clean polymorphic _type field after relation is removed

296 Views Asked by At

I have Article model which can belong to either ServiceType or ServiceAddonType through polymorphic association (optional):

enter image description here

Models

Article

class Article < ApplicationRecord
  belongs_to :accountable, polymorphic: true, optional: true
end

ServiceType

class ServiceType < ApplicationRecord
  has_many :articles, as: :accountable
end

ServiceAddonType

class ServiceAddonType < ApplicationRecord
  has_many :articles, as: :accountable
end

Article migration

class CreateArticles < ActiveRecord::Migration[5.1]
  def change
    create_table :articles, id: :uuid do |t|
      t.references :accountable, polymorphic: true, index: true, type: :uuid
    end
  end
end

Problem:

When adding Article to ServiceType, everything works as expected. The polymorphic fields get the following values:

accountable_id: service_type.id
accountable_type: 'ServiceType'

After removing Article from ServiceType, the fields' values are:

accountable_id: nil
accountable_type: 'ServiceType'

Why? How can I fix this properly so that accountable_type would also be nil after removing the relation? I made a before_save callback to Article model which would clean the field if accountable_id is nil, but this, as a matter of fact, doesn't seem to have any effect at all ('ServiceType' still remains as the _type value). I guess it might be because article update doesn't go through ActiveRecord at that point but through raw SQL-query when I remove it from ServicType. Therefore the callback never gets called. Anyhow I also feel there should be some better ways for achieving this.


EDIT1:

Removing article from service_type:

ServiceType form (slim-syntax): This gives me a box with list of articles and their checkboxes which are already assigned to service type. By unchecking them and clicking submit on the form I can remove the relation.

- if service_type.persisted?
    .square-box.small-12.medium-5
      - if service_type.articles.present?
        h5= t('article.assigned')
        = f.association :articles, collection: service_type.articles, as: :check_boxes, label: false
      - else
        = t('service_type.no_articles_assigned_yet')

ServiceTypeController#update with strong_params:

def update
  if @service_type.update(service_type_params)
    redirect_to service_types_path, notice: t(
      'service_type.successful_update',
      service_type_name: @service_type.name
    )
  else
    redirect_to edit_service_type_path(@service_type), alert: @service_type.errors.full_messages.join(', ')
  end
end

private

def service_type_params
  params.require(:service_type).permit(:name, article_ids: [])
end

EDIT2:

More complete version of my article-model with the callback I tested. The callback actually gets called when relation is added but doesn't get called when the relation is removed.

class Article < ApplicationRecord
  belongs_to :accountable, polymorphic: true, optional: true

  default_scope { order(:name) }

  before_save :arrange_polymorphic_fields

  private

  def arrange_polymorphic_fields
    def arrange_polymorphic_fields
      !accountable_id.present? && self.accountable_type = nil
    end
  end
end

Rails log of removing the article from service-type:

Started PATCH "/service_types/acfe9618-69d4-4564-9ab5-65a50dc4b26a" for 127.0.0.1 at 2018-11-08 16:23:57 +0200
Processing by ServiceTypesController#update as HTML
  Parameters: {"utf8"=>"✓", "authenticity_token"=>"bbBWybKuXcwcYVZZqcL88VRY1U6puY9rz2TczyNEghlM+WIIf46lGtNYBk+sUITtLzhHWcNZl1FJL9s630rOgw==", "service_type"=>{"name"=>"Service-type 1", "article_ids"=>["", "8c2d53d8-9016-4e7b-85c8-d111797aa9d0", "84573dc7-c532-4055-a43d-ef5917cf1ec0", "0dc6da24-c221-4c24-9b22-c39b2f82a9d5", "f6a4dd0d-17bf-4bfd-9cc5-d871d23ad727", "25b25ed1-e4ae-43a6-87e7-f346def15aa5"]}, "id"=>"acfe9618-69d4-4564-9ab5-65a50dc4b26a"}
  ServiceType Load (0.9ms)  SELECT  "service_types".* FROM "service_types" WHERE "service_types"."id" = $1 ORDER BY "service_types"."name" ASC LIMIT $2  [["id", "acfe9618-69d4-4564-9ab5-65a50dc4b26a"], ["LIMIT", 1]]
   (0.5ms)  BEGIN
  Article Load (1.2ms)  SELECT "articles".* FROM "articles" WHERE "articles"."id" IN ('8c2d53d8-9016-4e7b-85c8-d111797aa9d0', '84573dc7-c532-4055-a43d-ef5917cf1ec0', '0dc6da24-c221-4c24-9b22-c39b2f82a9d5', 'f6a4dd0d-17bf-4bfd-9cc5-d871d23ad727', '25b25ed1-e4ae-43a6-87e7-f346def15aa5') ORDER BY "articles"."name" ASC
  Article Load (1.1ms)  SELECT "articles".* FROM "articles" WHERE "articles"."accountable_id" = $1 AND "articles"."accountable_type" = $2 ORDER BY "articles"."name" ASC  [["accountable_id", "acfe9618-69d4-4564-9ab5-65a50dc4b26a"], ["accountable_type", "ServiceType"]]
  SQL (1.7ms)  UPDATE "articles" SET "accountable_id" = NULL WHERE "articles"."id" IN (SELECT "articles"."id" FROM "articles" WHERE "articles"."accountable_id" = $1 AND "articles"."accountable_type" = $2 AND "articles"."id" = '542803bf-73ee-496f-a7e7-077858925245' ORDER BY "articles"."name" ASC)  [["accountable_id", "acfe9618-69d4-4564-9ab5-65a50dc4b26a"], ["accountable_type", "ServiceType"]]
  ServiceType Exists (1.2ms)  SELECT  1 AS one FROM "service_types" WHERE "service_types"."name" = $1 AND ("service_types"."id" != $2) LIMIT $3  [["name", "Service-type 1"], ["id", "acfe9618-69d4-4564-9ab5-65a50dc4b26a"], ["LIMIT", 1]]
   (2.4ms)  COMMIT
Redirected to http://localhost:3000/service_types
Completed 302 Found in 31ms (ActiveRecord: 9.1ms)
2

There are 2 best solutions below

2
Maxence On

That is strange it is not called when the relation is removed. Anyway your callback doesn't seem to do anything at the moment.

Maybe you can try changing the below:

def arrange_polymorphic_fields
    def arrange_polymorphic_fields
      !accountable_id.present? && self.accountable_type = nil
    end
  end

to

def arrange_polymorphic_fields
   unless self.accountable_id.present?
      self.accountable_type = nil
   end
end

And trying again to remove a relationship.

(Actually I see what you are trying to do with !accountable_id.present? && self.accountable_type = nil but I am not a great fan of mixing a conditional statement with actual assignment. I prefer writing dead simple code if not very concise.. And then we can go further is this is not where the problem comes from..)

0
Andres On

This is not the answer I am looking for but it solves my problem so far.

I assume this is a wider Rails problem because of this old post and this github issue. I did try dependent: :nullify) and for my Rails 5.1.6 it didn't work.

Therefore I came up with creating ArticleConcern and included it in ServiceType and ServiceAddonType models. Works as needed:

# frozen_string_literal: true

require 'active_support/concern'

module ArticleConcern
  extend ActiveSupport::Concern

  included do
    after_save -> { arrange_polymorphic_fields }
  end

  def arrange_polymorphic_fields
    Article.where(accountable_id: nil).where.not(accountable_type: nil).each do |a|
      a.update(accountable_type: nil)
    end
  end
end