On Thursday, February 26, 2015 at 9:03:21 AM UTC-8, Andrew Burleson wrote:
>
> I ran into something today I'm having a really hard time figuring out:
>
> I've got a model instance, `user`, and after fetching that user instance
> I'd like to make it available in a "read only" state, which is to say I
> don't want any 'downstream' code to be able to make changes to it. #freeze
> accomplishes this. It also makes sense to be to pass a frozen copy of the
> object off to another process, while retaining an unfrozen copy. Testing
> this led to some unexpected results:
>
> The relevant portion of my tests looks like this:
> DB.transaction(:rollback => :always, :auto_savepoint=>true) do
> user = FinishSignup.call(...)
> assert_equal u, User.order(:id).last
> assert_equal u.properties.first, Property.order(:id).last
> end
>
> The `FinishSignup` call creates a user and property instance. It dups the
> user, freezes the dup, and passes this frozen dup on to some observers. The
> function then returns the original user instance.
>
> The test fails on the last assertion, with a stack trace like this:
>
> RuntimeError: can't modify frozen Hash
>> ~/.rbenv/versions/2.2.0/lib/ruby/gems/2.2.0/gems/sequel-4.19.0/lib/sequel/model/associations.rb:2240:in
>>
>> `load_associated_objects'
>> ~/.rbenv/versions/2.2.0/lib/ruby/gems/2.2.0/gems/sequel-4.19.0/lib/sequel/plugins/association_proxies.rb:83:in
>>
>> `method_missing'
>> ~/project/test/services/finish_signup_test.rb:18:in `block (3 levels) in
>> <top (required)>'
>> ~/project/test/test_helper.rb:31:in `block in transaction'
>> ~/.rbenv/versions/2.2.0/lib/ruby/gems/2.2.0/gems/sequel-4.19.0/lib/sequel/database/transactions.rb:119:in
>>
>> `_transaction'
>> ~/.rbenv/versions/2.2.0/lib/ruby/gems/2.2.0/gems/sequel-4.19.0/lib/sequel/database/transactions.rb:100:in
>>
>> `block in transaction'
>> ~/.rbenv/versions/2.2.0/lib/ruby/gems/2.2.0/gems/sequel-4.19.0/lib/sequel/database/connecting.rb:250:in
>>
>> `block in synchronize'
>> ~/.rbenv/versions/2.2.0/lib/ruby/gems/2.2.0/gems/sequel-4.19.0/lib/sequel/connection_pool/threaded.rb:98:in
>>
>> `hold'
>> ~/.rbenv/versions/2.2.0/lib/ruby/gems/2.2.0/gems/sequel-4.19.0/lib/sequel/database/connecting.rb:250:in
>>
>> `synchronize'
>> ~/.rbenv/versions/2.2.0/lib/ruby/gems/2.2.0/gems/sequel-4.19.0/lib/sequel/database/transactions.rb:89:in
>>
>> `transaction'
>> ~/project/test/test_helper.rb:30:in `transaction'
>> ~/project/test/services/finish_signup_test.rb:9:in `block (2 levels) in
>> <top (required)>'
>> ~/.rbenv/versions/2.2.0/lib/ruby/gems/2.2.0/gems/sequel-4.19.0/lib/sequel/model/associations.rb:2240:in
>>
>> `load_associated_objects'
>> ~/.rbenv/versions/2.2.0/lib/ruby/gems/2.2.0/gems/sequel-4.19.0/lib/sequel/plugins/association_proxies.rb:83:in
>>
>> `method_missing'
>> ~/project/test/services/finish_signup_test.rb:18:in `block (3 levels) in
>> <top (required)>'
>> ~/project/test/test_helper.rb:31:in `block in transaction'
>> ~/.rbenv/versions/2.2.0/lib/ruby/gems/2.2.0/gems/sequel-4.19.0/lib/sequel/database/transactions.rb:119:in
>>
>> `_transaction'
>> ~/.rbenv/versions/2.2.0/lib/ruby/gems/2.2.0/gems/sequel-4.19.0/lib/sequel/database/transactions.rb:100:in
>>
>> `block in transaction'
>> ~/.rbenv/versions/2.2.0/lib/ruby/gems/2.2.0/gems/sequel-4.19.0/lib/sequel/database/connecting.rb:250:in
>>
>> `block in synchronize'
>> ~/.rbenv/versions/2.2.0/lib/ruby/gems/2.2.0/gems/sequel-4.19.0/lib/sequel/connection_pool/threaded.rb:98:in
>>
>> `hold'
>> ~/.rbenv/versions/2.2.0/lib/ruby/gems/2.2.0/gems/sequel-4.19.0/lib/sequel/database/connecting.rb:250:in
>>
>> `synchronize'
>> ~/.rbenv/versions/2.2.0/lib/ruby/gems/2.2.0/gems/sequel-4.19.0/lib/sequel/database/transactions.rb:89:in
>>
>> `transaction'
>> ~/project/test/test_helper.rb:30:in `transaction'
>> ~/project/test/services/finish_signup_test.rb:9:in `block (2 levels) in
>> <top (required)>'
>
>
>
> More background, and research I did so far:
>
> The relevant portion of the model looks like this:
> class User < Sequel::Model
> one_to_many :properties
> end
>
> Inside the FinishSignup call looks basically like this:
>
> # Create User and Property
> user = User.create(...)
> property = Property.create(user: user, ...)
> trigger_event :user_created, user: user
>
> The trigger event method is a generic method that works like this:
> def trigger_event(name,**args)
> frozen_args = args.each.with_object({}){|(k,v),o| o[k] = v.dup.freeze}
> send(name,**frozen_args)
> end
>
> ^ where send actually calls the event trigger.
>
> The reason this is most confusing is I can't duplicate it in the console.
> I've tried every variation of this procedure I can think of, creating a
> user, creating a property, duping the user, freezing the dup, etc., and in
> every case I'm able to call `original_user.properties.first` with no
> runtime error. I've tried this in the console inside and outside of
> transactions etc.
>
> So, I think something else must be going on in the Sequel Internals that
> I'm not aware of which is the reason this specific case won't work, but I'm
> having a terrible time figuring out what it is.
>
> Any insight you can share?
>
If you look at the line of code that raises the error:
associations[name] = objs unless frozen?
It's already supposed to check if the object is frozen. However, from your
description, I think the problem is that dup doesn't duplicate the
associations hash, when it should. This should fix it:
class Sequel::Model
def initialize_copy(other)
super
@associations = @associations.dup if @associations
self
end
end
I'll make sure that makes it into the next version.
Thanks,
Jeremy
--
You received this message because you are subscribed to the Google Groups
"sequel-talk" group.
To unsubscribe from this group and stop receiving emails from it, send an email
to [email protected].
To post to this group, send email to [email protected].
Visit this group at http://groups.google.com/group/sequel-talk.
For more options, visit https://groups.google.com/d/optout.