I am using friendly_id gem for slugging my models. Since the slug has to be unique when i enter the same data to check i get a long hashed appending in the slug.
Today I came accross this issue and while other answer helped me get started, I wasn't satisfied because, like you, I wanted to have the slugs appear in sequence like explore
, explore-2
, explore-3
.
So, here's how I fixed it:
class Thing < ActiveRecord::Base
extend FriendlyId
friendly_id :slug_candidates, use: :slugged
validates :name, presence: true, uniqueness: { case_sensitive: false }
validates :slug, uniqueness: true
def slug_candidates
[:name, [:name, :id_for_slug]]
end
def id_for_slug
generated_slug = normalize_friendly_id(name)
things = self.class.where('slug REGEXP :pattern', pattern: "#{generated_slug}(-[0-9]+)?$")
things = things.where.not(id: id) unless new_record?
things.count + 1
end
def should_generate_new_friendly_id?
name_changed? || super
end
end
I used uniqueness validation for :slug
just in case this model is being used in concurrent code.
Here you can see it working:
irb(main):001:0> Thing.create(name: 'New thing')
(0.1ms) begin transaction
(0.2ms) SELECT COUNT(*) FROM "things" WHERE (slug REGEXP 'new-thing(-[0-9]+)?$')
Thing Exists (0.1ms) SELECT 1 AS one FROM "things" WHERE ("things"."id" IS NOT NULL) AND "things"."slug" = ? LIMIT 1 [["slug", "new-thing"]]
Thing Exists (0.1ms) SELECT 1 AS one FROM "things" WHERE LOWER("things"."name") = LOWER('New thing') LIMIT 1
Thing Exists (0.1ms) SELECT 1 AS one FROM "things" WHERE "things"."slug" = 'new-thing' LIMIT 1
SQL (0.4ms) INSERT INTO "things" ("name", "slug") VALUES (?, ?) [["name", "New thing"], ["slug", "new-thing"]]
(115.7ms) commit transaction
=> #
irb(main):002:0> Thing.create(name: 'New thing')
(0.2ms) begin transaction
(0.9ms) SELECT COUNT(*) FROM "things" WHERE (slug REGEXP 'new-thing(-[0-9]+)?$')
Thing Exists (0.1ms) SELECT 1 AS one FROM "things" WHERE ("things"."id" IS NOT NULL) AND "things"."slug" = ? LIMIT 1 [["slug", "new-thing"]]
Thing Exists (0.1ms) SELECT 1 AS one FROM "things" WHERE ("things"."id" IS NOT NULL) AND "things"."slug" = ? LIMIT 1 [["slug", "new-thing-2"]]
Thing Exists (0.1ms) SELECT 1 AS one FROM "things" WHERE LOWER("things"."name") = LOWER('New thing') LIMIT 1
Thing Exists (0.1ms) SELECT 1 AS one FROM "things" WHERE "things"."slug" = 'new-thing-2' LIMIT 1
(0.1ms) rollback transaction
=> #
irb(main):003:0> Thing.create(name: 'New-thing')
(0.2ms) begin transaction
(0.5ms) SELECT COUNT(*) FROM "things" WHERE (slug REGEXP 'new-thing(-[0-9]+)?$')
Thing Exists (0.1ms) SELECT 1 AS one FROM "things" WHERE ("things"."id" IS NOT NULL) AND "things"."slug" = ? LIMIT 1 [["slug", "new-thing"]]
Thing Exists (0.1ms) SELECT 1 AS one FROM "things" WHERE ("things"."id" IS NOT NULL) AND "things"."slug" = ? LIMIT 1 [["slug", "new-thing-2"]]
Thing Exists (0.3ms) SELECT 1 AS one FROM "things" WHERE LOWER("things"."name") = LOWER('New-thing') LIMIT 1
Thing Exists (0.3ms) SELECT 1 AS one FROM "things" WHERE "things"."slug" = 'new-thing-2' LIMIT 1
SQL (0.4ms) INSERT INTO "things" ("name", "slug") VALUES (?, ?) [["name", "New-thing"], ["slug", "new-thing-2"]]
(108.9ms) commit transaction
=> #
irb(main):004:0> Thing.create(name: 'New!thing')
(0.2ms) begin transaction
(0.6ms) SELECT COUNT(*) FROM "things" WHERE (slug REGEXP 'new-thing(-[0-9]+)?$')
Thing Exists (0.0ms) SELECT 1 AS one FROM "things" WHERE ("things"."id" IS NOT NULL) AND "things"."slug" = ? LIMIT 1 [["slug", "new-thing"]]
Thing Exists (0.1ms) SELECT 1 AS one FROM "things" WHERE ("things"."id" IS NOT NULL) AND "things"."slug" = ? LIMIT 1 [["slug", "new-thing-3"]]
Thing Exists (0.1ms) SELECT 1 AS one FROM "things" WHERE LOWER("things"."name") = LOWER('New!thing') LIMIT 1
Thing Exists (0.1ms) SELECT 1 AS one FROM "things" WHERE "things"."slug" = 'new-thing-3' LIMIT 1
SQL (0.1ms) INSERT INTO "things" ("name", "slug") VALUES (?, ?) [["name", "New!thing"], ["slug", "new-thing-3"]]
(112.4ms) commit transaction
=> #
irb(main):005:0>
Also, if you use sqlite3 adapter you'll need to install sqlite3_ar_regexp
gem (it won't be very fast, because SQLite doesn't have REGEXP() and it evaluates Ruby code instead).