friendly_id icon indicating copy to clipboard operation
friendly_id copied to clipboard

History creation timing

Open maxime-carbonneau opened this issue 8 months ago • 9 comments

With Rails 8

class Book < ActiveRecord::Base
  has_many :chapters
  include FriendlyId
  friendly_id :title , :use => [:slugged, :history]
end

class Chapter < ActiveRecord::Base
  include FriendlyId
  friendly_id :title , :use => [:slugged, :history]
end

book = Book.new(title: "A book")

book.chapters << Chapter.new(title: "A Chapter")
book.chapters << Chapter.new(title: "A Chapter")

book.save!

Give me

ActiveRecord::RecordNotUnique

According to

# lib/friendly_id/history.rb
72:    def self.included(model_class)
73:      model_class.class_eval do
74:        has_many :slugs, -> { order(id: :desc) }, **{
75:          as: :sluggable,
76:          dependent: @friendly_id_config.dependent_value,
77:          class_name: Slug.to_s
78:        }
79:
80:        after_save :create_slug
81:      end
82:    end

the history record is created on a after_save callback instead of around_save callback.

Is it a bug or a feature?

maxime-carbonneau avatar May 06 '25 18:05 maxime-carbonneau

You're trying to create two chapters with the same title and save them at the same time, so the second chapter would raise title not unique, is that unexpected? Do you expect the second slug to have a suffix on it?

Thanks!

parndt avatar May 06 '25 21:05 parndt

Yes, I was expecting the second slug to have a suffix on it. It was my understanding of the uniqueness feature (https://norman.github.io/friendly_id/file.Guide.html#Uniqueness). But, I could have misunderstand the doc.

maxime-carbonneau avatar May 06 '25 21:05 maxime-carbonneau

As an experiment, does this work:

class Book < ActiveRecord::Base
  has_many :chapters
  include FriendlyId
  friendly_id :title , :use => [:slugged, :history]
end

class Chapter < ActiveRecord::Base
  include FriendlyId
  friendly_id :title , :use => [:slugged, :history]
end

book = Book.new(title: "A book")

book.chapters << Chapter.new(title: "A Chapter")

book.save!

book.chapters << Chapter.new(title: "A Chapter")

book.save!

just to narrow down the issue

parndt avatar May 06 '25 22:05 parndt

Yes, it work if I slip into 2 saves

Adding around_create callback fix the issue.


class Chapter < ActiveRecord::Base
  include FriendlyId
  friendly_id :title , :use => [:slugged, :history]

  around_create :fix_friendly_id_with_history

  def fix_friendly_id_with_history
    yield
    create_slug
  end
end

The after_save seems to be call too far in the process.

maxime-carbonneau avatar May 07 '25 01:05 maxime-carbonneau

Thanks for confirming! Seems like maybe we should replace our after_save hook with an around_save here: https://github.com/norman/friendly_id/blob/40b899a547624a50dbe44c5f337ef393e04a9152/lib/friendly_id/history.rb#L80

what do you think?

parndt avatar May 07 '25 03:05 parndt

After some more thought, I think that you should keep after_save and add around_create instead of around_save.

If I understand correctly around_save return from the yield after all the associations of the object are save, while around_create return from yield before the associations of the object are save.

maxime-carbonneau avatar May 07 '25 04:05 maxime-carbonneau

I over simplify the pseudo code to get the error. Sorry. It had to be a kind of linked list where next nodes hade the same slugs.

class Book < ActiveRecord::Base
  has_many :chapters
  include FriendlyId
  friendly_id :title , :use => [:slugged, :history]
end

class Chapter < ActiveRecord::Base
  belongs_to :book

  belongs_to :next, class_name: "Chapter"

  include FriendlyId
  friendly_id :title , :use => [:slugged, :history]
end

book = Book.new(title: "A book")

book.chapters.build(title: "A chapter")
book.chapters.build(title: "A chapter")
book.chapters[0].next = book.chapters[1]

book.save!

I make tests with around_create and after_create. It only works with around_create.

maxime-carbonneau avatar May 07 '25 12:05 maxime-carbonneau

If I understand correctly around_save return from the yield after all the associations of the object are save, while around_create return from yield before the associations of the object are save.

Good info, thanks! But I'm not sure that we can use around_create because then when the record is updated we wouldn't re-evaluate any slugs?

parndt avatar May 07 '25 22:05 parndt

If you're interested in spending the time on it, opening a pull request with a failing test for the use case you're aiming for (it's OK if it just a test that fails) would be very helpful in terms of identifying a good solution for this?

parndt avatar May 07 '25 22:05 parndt