Summary
After calling the as_json method on a model, attempting to remove tags using label_ids = [] does not work - the tags remain attached to the model.
Environment
- acts-as-taggable-on: v6.0.0 ~ v12.0.0
- Rails: 5-2-stable ~ 8.0-stable
- Ruby: 2.7.8 ~ 3.4.1
Steps to Reproduce
- Create a model with custom tag class (
Label)
- Set up
acts_as_taggable_on :labels
- Add tags using
label_ids
- Call
as_json method
- Try to remove tags using
label_ids = []
Reproduction Code
- Save following code as
issue_cannot_save_tags_after_as_json.rb in repository root.
$ ruby issue_cannot_save_tags_after_as_json.rb
# frozen_string_literal: true
begin
require "bundler/inline"
rescue LoadError => e
$stderr.puts "Bundler version 1.10 or later is required. Please update your Bundler"
raise e
end
gemfile = File.read(File.expand_path("Gemfile", __dir__)).strip
gemfile(true) do
source "https://rubygems.org"
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
gem "logger"
# gem "rails", github: "rails/rails", branch: "5-1-stable"
# gem "rails", github: "rails/rails", branch: "5-2-stable"
# gem "rails", github: "rails/rails", branch: "6-0-stable"
# gem "rails", github: "rails/rails", branch: "6-1-stable"
# gem "rails", github: "rails/rails", branch: "7-0-stable"
# gem "rails", github: "rails/rails", branch: "7-1-stable"
# gem "sqlite3", '~> 1.4.4'
gem "rails", github: "rails/rails", branch: "8-0-stable"
gem "sqlite3", '~> 2.1.0'
gem 'debug'
gem 'pry-byebug'
gem 'stringio'
gem 'acts-as-taggable-on', path: '.'
end
require "active_record"
require "minitest/autorun"
require "logger"
# Database connection setup
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Base.logger = Logger.new(STDOUT)
# Run acts-as-taggable-on migrations
ActiveRecord::Schema.define do
# Create acts-as-taggable-on tables
create_table :tags, force: true do |t|
t.string :name
t.integer :taggings_count, default: 0
t.string :type
end
add_index :tags, ['name'], name: 'index_tags_on_name' # , unique: true
create_table :taggings, force: true do |t|
t.integer :tag_id
t.string :taggable_type
t.integer :taggable_id
t.string :tagger_type
t.integer :tagger_id
t.string :context, limit: 128
t.string :tenant , limit: 128
t.datetime :created_at
end
add_index :taggings,
['tag_id', 'taggable_id', 'taggable_type', 'context', 'tagger_id', 'tagger_type'],
unique: true, name: 'taggings_idx'
add_index :taggings, :tag_id , name: 'index_taggings_on_tag_id'
# Test model tables
create_table :articles, force: true do |t|
t.string :title
t.text :content
t.timestamps
t.text :cached_label_list
end
end
# Model definitions
class Article < ActiveRecord::Base
acts_as_taggable_on :labels
# # Adding this fixes the issue
# def as_json(options = {})
# # Exclude label_list
# super(options.merge(except: :label_list))
# end
end
class Label < ActsAsTaggableOn::Tag
end
# Test class
class ActsAsTaggableOnTest < Minitest::Test
def test_update_article_with_empty_labels
article = Article.create!(title: "Test Title", content: "Test Content")
# Create labels
label1 = Label.find_or_create_by!(name: "Label1")
label2 = Label.find_or_create_by!(name: "Label2")
# Update article (set labels)
article.update!(
label_ids: [label1.id, label2.id]
)
article = Article.find(article.id)
article.reload
# ↓ Removing this makes the test pass
article.as_json # Accesses article.label_list, causing INSERT/DELETE operations in after_save
# label_ids immediately performs INSERT/DELETE on labels + then enters after_save
article.update!(label_ids: [])
article.reload
assert_empty article.label_list
# ActsAsTaggableOnTest#test_update_article_with_empty_labels [issue_cannot_save_tags_after_as_json.rb:126]:
# Expected ["Label1", "Label2"] to be empty.
end
end
Expected Behavior
Tags should be completely removed when setting label_ids = []
Actual Behavior
After calling as_json, tags are not removed when using label_ids = []
Workarounds
Exclude label_list from as_json
class Article < ActiveRecord::Base
acts_as_taggable_on :labels
def as_json(options = {})
super(options.merge(except: :label_list))
end
end
Analysis
- The
as_json method accesses label_list, which modifies the internal state
- Subsequent updates using
label_ids cause INSERT/DELETE operations in the after_save callback to malfunction
- Using
label_list works correctly
Additional Information
See the attached issue_cannot_save_tags_after_as_json.rb for the complete reproduction test code.
Summary
After calling the
as_jsonmethod on a model, attempting to remove tags usinglabel_ids = []does not work - the tags remain attached to the model.Environment
Steps to Reproduce
Label)acts_as_taggable_on :labelslabel_idsas_jsonmethodlabel_ids = []Reproduction Code
issue_cannot_save_tags_after_as_json.rbin repository root.$ ruby issue_cannot_save_tags_after_as_json.rbExpected Behavior
Tags should be completely removed when setting
label_ids = []Actual Behavior
After calling
as_json, tags are not removed when usinglabel_ids = []Workarounds
Exclude label_list from as_json
Analysis
as_jsonmethod accesseslabel_list, which modifies the internal statelabel_idscause INSERT/DELETE operations in theafter_savecallback to malfunctionlabel_listworks correctlyAdditional Information
See the attached
issue_cannot_save_tags_after_as_json.rbfor the complete reproduction test code.