rails / globalid

Identify app models with a URI

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

DeserializationError when not found with default_scope

philipgiuliani opened this issue · comments

When i have set a default_scope to my model, GlobalID can't find it. https://github.com/rails/globalid/blob/master/lib/global_id/locator.rb#L130

Error while trying to deserialize arguments: Couldn't find Image with 'id'=75

Maybe it should always be set to unscoped?

If you have a default_scope it would make more sense to me that it should be applied by default 😄

But I haven't seen the code. Can you paste something that approximates the scenario?

I dont think that it should be applied for mailers/jobs (internal logic)
A little example for my use case:

# User Model
class User < ActiveRecord::Base
  default_scope { where(status: :active) }
  enum status: [:active, :blocked]
end

# Some method somewhere
def report_user
  user = User.find(params[:id])
  user.blocked!

  AdminMailer.user_blocked(user).deliver_later
end

This example will fail, because the default scope is blocked of course. Currently i am passing user_id instead of the user instead as workaround.

What do you think?

Default scope should be applied here as well. Otherwise applying it sporadically and nondeterministically doesn't make sense to me.

The way I read your code, you only want active users in 99% of the cases, but in this one case where you're dealing with blocked users you should use unscope. ❤️

Thanks for the answer and explanation. With using unscope you mean passing the user_id instead, right?

The way I read your code, you only want active users in 99% of the cases, but in this one case where you're dealing with blocked users you should use unscope.

meaning don't use GlobalID (aka "good ol pass by id")? or there's a way to specify for unscope?

I mean use the unscoped method:

def report_user
  User.unscoped do  
    user = User.find(params[:id])
    user.blocked!

    AdminMailer.user_blocked(user).deliver_later
  end
end

Eh, I just figured that might not work when GlobalID later calls find because deliver_later returns immediately.

Yes thats what i meant.. :D

Yeah.. this is tricky. Do we need some sort of AR::Base.definitely_find(id)? I certainly agree that unlike a random User.find(params[:id]), if we know we started out with an extant User instance, it does rather seem that it's on us to make sure we manage to get it back again.

Anything we do here is going to change the API required from a Global ID-supporting model class... but I think directly relying on unscoped would be unreasonably broad. Maybe we should first try for no_really_do_actually_find, and if it doesn't respond to that, fall back to regular find? Without such a fallback, we would seem to have a rather unpleasant versioning/compatibility issue.

In GlobalID there's a default AR finder:

      class ActiveRecordFinder
        def locate(gid)
          gid.model_class.find gid.model_id
        end
      end

      mattr_reader(:default_locator) { ActiveRecordFinder.new }

This can be changed to model_class.unscoped.find gid.model_id.

The ActiveRecordFinder is private and you shouldn't override locate in it. Instead you can use our custom app locators for this. Substitute bcx for your app name here:

GlobalID::Locator.use 'bcx' do |gid|
  gid.model_class.unscoped.find gid.model_gid
end

@kaspth I meant change that in GlobalID (if it's AR only code) versus @matthewd 's idea on creating AR::Base.definitely_find(id)

I also think changing the default ActiveRecordFinder would be the cleanest and simplest way. Since its only for AR there should be no compability issues.

Well, the name is a little misleading. It's the default locator and is for any model that implements find which takes an id.

find is the contract for models to be locatable through Global ID. So if we add unscoped or something that's another method for models to implement. Which Active Record models do by default, but it might not make sense for a lot of other models.

@kaspth we could Have a DefaultFinder and an ActiveRecordFinder and the default_locator can be converted to default_locator_for(gid) and based on the gid.model_class decide if it should use DefaultFinder or ActiveRecordFinder

Maybe we could do it as:

class DefaultFinder
  def locate(gid)
    gid.model_class.find(gid.model_id)
  end
end

class ActiveRecordFinder < DefaultFinder
  def locate(gid)
    gid.model_class.unscoped { super }
  end
end

@matthewd what do you think about this?