[Suggestion] Allow different primary_ids such as UUID
amerritt14 opened this issue · comments
I have a similar concept that I use in some apps when syncing data. I call it a source_token
The format is almost identical, except that it uses UUID instead of ID. Would there be interest in allowing an optional parameter which would serialize the uuid
instead?
I'm imagining something along these lines:
User.find_by(uuid: "abc-123-uuid").to_global_id(primary_key: :uuid).to_s
=> "gid://myapp/User/uuid/abc-123-uuid"
The presence of a value between the class
and id
would indicate a different method of lookup. In order to maintain backwards compatibility, it would assume id
if no other value is specified.
I'm happy to take a stab at implementing but I would love any insight around the acceptance of such a feature.
TLDR: I tried to work this out thinking it would be a small and simple change, however the current code expects a specific URL structure (gid:://app/Model/1). Adding this feature will break a lot of things. I wonder if I should move forward with a feature like this at this state.
I stumbled into needing this as well. I was trying to draft something in the source code, however I got a couple thoughts. Having a custom model field for the id like primary_key
in your example will demand quite a few changes:
- on the Locator's code as everything is done through model's
find
. We would need to change everything tofind_by primary_key: value
globalid/lib/global_id/locator.rb
Line 128 in 0ed21fb
- URI::GID Expects a specific URL structure, I am not sure how it would break it
I drafted it up something here: https://github.com/rails/globalid/compare/main...rafaelxy:custom-id-in-uri-gid?expand=1
and I got three apparent failures:
Failure:
URI::GIDAppValidationTest#test_apps_containing_non_alphanumeric_characters_are_invalid [/Users/rafaelrichard/src/github.com/rafaelxy/globalid/test/cases/uri_gid_test.rb:122]:
ArgumentError expected but nothing was raised.
rails test test/cases/uri_gid_test.rb:121
................................................F
Failure:
URI::GIDValidationTest#test_too_many_model_ids [/Users/rafaelrichard/src/github.com/rafaelxy/globalid/test/cases/uri_gid_test.rb:92]:
URI::InvalidComponentError expected but nothing was raised.
rails test test/cases/uri_gid_test.rb:91
..F
Failure:
GlobalLocatorTest#test_by_invalid_GID_URI_returns_nil [/Users/rafaelrichard/src/github.com/rafaelxy/globalid/test/cases/global_locator_test.rb:172]:
Expected #<Person:0x00007f8037a2da00 @id="2", @other_id=nil> to be nil.
rails test test/cases/global_locator_test.rb:168
I predict that if I keep going it will break more and more tests on each class I have to meddle since this new model_id_name
param will be rippling together with the other URI parts. 🤔
Thanks for taking a look at this, and even working up some code changes!
I have spent a small bit of time working on this as well, feel free to browse my draft PR
With my current implementation (including this gem locally in a project for testing) It still works as it did previously. Not including a primary_key
argument just works. Tests, however are failing due to the nature of how Person.find
is stubbed to invoke Person.new
So there's still some work to make those pass.
The issue I found is that when I introduce a primary_key, it's changing the URI structure, which means the COMPONENT
const needs to include the new key conditionally if the primary_key != :id
Basically, If I update the URI expectations to include the primary_key, it breaks backwards compatibility, but if I don't, it doesn't recognize the new URI structure and errors out.
Admittedly, I haven't spent much time working on this, so I'll take a look at your work and see if I'm able to get something more solid put together.
Hey folks, I noticed this issue and wanted to mention that the current version of the gem should support UUID without any changes required. This should happen due to Active Record
having a special meaning for the #id
method. So as long as an Active Record model has Model.primary_key = :uuid
the #id
method will return the value of the uuid column along with find()
working perfectly fine
The only thing that doesn't work is the locate_many
with ignore_missing
since the gem hardcoded the id
column lookup here
globalid/lib/global_id/locator.rb
Line 145 in 86ddaae
but it should be easily fixable if we change it to where(model.primary_key => ids)
which will enforce primary_key
to exist on models but that should be fine since Active Record already implements it
class ModelWithUuidPk
include ActiveModel::Model
include GlobalID::Identification
UUID = "550e8400-e29b-41d4-a716-446655440000"
def id
UUID
end
def ==(other)
id == other.id
end
def self.find(uuid)
raise "can't find" if uuid != UUID
self.new
end
end
test 'uuid primary key' do
model = ModelWithUuidPk.new
gid = GlobalID.create(model)
assert_equal "gid://bcx/GlobalIDParamEncodedTest::ModelWithUuidPk/#{ModelWithUuidPk::UUID}", gid.to_s
found = GlobalID.find(gid.to_param)
assert_kind_of ModelWithUuidPk, found
assert_equal model.to_gid.find, found
# The only thing that is broken
GlobalID::Locator.locate_many([ model.to_gid, model.to_gid ])
end
So I just wanted to share that most of the common use-cases should already be supported. Let me know if that's not exactly the functionality you were looking for. Thanks!
This is fixed now
I ran across this issue and it's not clear to me that the fix actually addresses @amerritt14's original need. I feel like it's addressing @nvasilevski's comment which feels tangentially related
I think I have a similar need to the spirit of @amerritt14's original comment which is that we have a subset of models which have autoincremented primary keys, but also have a UUID column named external_id
which we use more publicly.
The hardcoding of model#id
in URI::GID.create
feels like my blocker.
Given an example user:
User.pluck(:id, :external_id).last
=> [4224, "K0-HUZMT-FT0-S3D"]
Then user.to_gid.to_s
will give me "gid://example-app/User/4224"
But if I could provide an argument somewhere to GlobalID to send a different method to model
via URI::GID.create
, I could get the string I'm hoping for.
gid = URI::GID.build(
app: "example-app",
model_name: user.class.name,
model_id: user.external_id, # this is the important difference
params: nil
)
gid.to_s
=> "gid://example-app/User/K0-HUZMT-FT0-S3D"
I can live with defining a custom loader. In fact I can see that as likely being desirable.
I'm happy to work on drafting a PR for this feels aligned with maintainers' thoughts on what the library could support. Thoughts @rafaelfranca?