Active Delegate: fails to delegate on collections
Reported by Jereme Claussen | October 5th, 2007 @ 04:27 PM
I haven't tested this, but after a quick code review, I can see that it is likely to have the same problem I ran into when doing a similar implementation.
Collections have a habit of doing a delete_all when you perform an absolute assignment such as:
user.posts = [post1,post2,post3]
Deletes don't have callbacks, so in my case the system attempts to delete from the mirror, which is readonly and fails.
A developer can specify a callback when defining a habtm or has_many relationship, so one solution is to provide a method that can be put there.
Another solution could be to intercept the delete call somehow with an override.
I have no suggested patch at the time.
Comments and changes to this ticket
-
Robby Russell October 5th, 2007 @ 04:30 PM
- → State changed from new to open
-
Robby Russell October 5th, 2007 @ 04:32 PM
- → Assigned user changed from to Robby Russell
Yeah, the initial version that I worked on was used on a few isolated models without needing associations. This definitely boosts the complexity up a bit. I'll think it over and see what I can come up with.
-
Robby Russell October 5th, 2007 @ 04:35 PM
Jereme.
Can you confirm that the following wouldn't work?
delegates_connection_to _database, => [, , , _all](added
_all) -

Jereme Claussen October 10th, 2007 @ 04:32 PM
I have
class InternalPermission < ActiveRecord::Base delegates_connection_to :master_database, :on => [:create, :save, :destroy, :destroy_all] has_and_belongs_to_many :internal_users endand
class InternalUser < ActiveRecord::Base delegates_connection_to :master_database, :on => [:create, :save, :destroy, :destroy_all] has_and_belongs_to_many :internal_permissions endWhen ActiveRecord issues the delete statment against the internal_permissions_internal_users table, it doesn't switch to the master table. This causes an error.
ActiveRecord::StatementInvalid (Mysql::Error: Access denied for user 'replaced'@'%' to database 'some_database': DELETE FROM internal_permissions_internal_users WHERE internal_user_id = 1 AND internal_permission_id IN (10)):
/usr/lib/ruby/gems/1.8/gems/activerecord-1.15.3/lib/active_record/connection_adapters/abstract_adapter.rb:128:in `log'
/usr/lib/ruby/gems/1.8/gems/activerecord-1.15.3/lib/active_record/connection_adapters/mysql_adapter.rb:243:in `execute'
/usr/lib/ruby/gems/1.8/gems/activerecord-1.15.3/lib/active_record/associations/has_and_belongs_to_many_association.rb:144:in `delete_records'
/usr/lib/ruby/gems/1.8/gems/activerecord-1.15.3/lib/active_record/associations/association_collection.rb:59:in `delete'
/usr/lib/ruby/gems/1.8/gems/activerecord-1.15.3/lib/active_record/connection_adapters/abstract/database_statements.rb:59:in `transaction'
/usr/lib/ruby/gems/1.8/gems/activerecord-1.15.3/lib/active_record/transactions.rb:95:in `transaction'
/usr/lib/ruby/gems/1.8/gems/activerecord-1.15.3/lib/active_record/transactions.rb:121:in `transaction'
/usr/lib/ruby/gems/1.8/gems/activerecord-1.15.3/lib/active_record/associations/association_collection.rb:57:in `delete'
/usr/lib/ruby/gems/1.8/gems/activerecord-1.15.3/lib/active_record/associations/association_collection.rb:144:in `replace'
/usr/lib/ruby/gems/1.8/gems/activerecord-1.15.3/lib/active_record/connection_adapters/abstract/database_statements.rb:59:in `transaction'
/usr/lib/ruby/gems/1.8/gems/activerecord-1.15.3/lib/active_record/transactions.rb:95:in `transaction'
/usr/lib/ruby/gems/1.8/gems/activerecord-1.15.3/lib/active_record/transactions.rb:121:in `transaction'
/usr/lib/ruby/gems/1.8/gems/activerecord-1.15.3/lib/active_record/associations/association_collection.rb:143:in `replace'
/usr/lib/ruby/gems/1.8/gems/activerecord-1.15.3/lib/active_record/associations.rb:950:in `internal_permissions='
/usr/lib/ruby/gems/1.8/gems/activerecord-1.15.3/lib/active_record/associations.rb:960:in `send'
/usr/lib/ruby/gems/1.8/gems/activerecord-1.15.3/lib/active_record/associations.rb:960:in `internal_permission_ids='
/usr/lib/ruby/gems/1.8/gems/activerecord-1.15.3/lib/active_record/base.rb:1672:in `send'
/usr/lib/ruby/gems/1.8/gems/activerecord-1.15.3/lib/active_record/base.rb:1672:in `attributes='
/usr/lib/ruby/gems/1.8/gems/activerecord-1.15.3/lib/active_record/base.rb:1671:in `each'
/usr/lib/ruby/gems/1.8/gems/activerecord-1.15.3/lib/active_record/base.rb:1671:in `attributes='
.//app/controllers/admin/user_controller.rb:51:in `update'
.//app/controllers/admin/user_controller.rb:24:in `edit'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/base.rb:1095:in `send'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/base.rb:1095:in `perform_action_without_filters'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:632:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:638:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:438:in `call'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:637:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:619:in `perform_action_without_benchmark'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/benchmarking.rb:66:in `perform_action_without_rescue'
/usr/lib/ruby/1.8/benchmark.rb:293:in `measure'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/benchmarking.rb:66:in `perform_action_without_rescue'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/rescue.rb:83:in `perform_action'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/base.rb:430:in `send'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/base.rb:430:in `process_without_filters'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:624:in `process_without_session_management_support'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/session_management.rb:114:in `process'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/base.rb:330:in `process'
/usr/lib/ruby/gems/1.8/gems/rails-1.2.3/lib/dispatcher.rb:41:in `dispatch'
/usr/lib/ruby/gems/1.8/gems/mongrel-1.0.1/lib/mongrel/rails.rb:78:in `process'
/usr/lib/ruby/gems/1.8/gems/mongrel-1.0.1/lib/mongrel/rails.rb:76:in `synchronize'
/usr/lib/ruby/gems/1.8/gems/mongrel-1.0.1/lib/mongrel/rails.rb:76:in `process'
/usr/lib/ruby/gems/1.8/gems/mongrel-1.0.1/lib/mongrel.rb:618:in `process_client'
/usr/lib/ruby/gems/1.8/gems/mongrel-1.0.1/lib/mongrel.rb:617:in `each'
/usr/lib/ruby/gems/1.8/gems/mongrel-1.0.1/lib/mongrel.rb:617:in `process_client'
/usr/lib/ruby/gems/1.8/gems/mongrel-1.0.1/lib/mongrel.rb:736:in `run'
/usr/lib/ruby/gems/1.8/gems/mongrel-1.0.1/lib/mongrel.rb:736:in `initialize'
/usr/lib/ruby/gems/1.8/gems/mongrel-1.0.1/lib/mongrel.rb:736:in `new'
/usr/lib/ruby/gems/1.8/gems/mongrel-1.0.1/lib/mongrel.rb:736:in `run'
/usr/lib/ruby/gems/1.8/gems/mongrel-1.0.1/lib/mongrel.rb:720:in `initialize'
/usr/lib/ruby/gems/1.8/gems/mongrel-1.0.1/lib/mongrel.rb:720:in `new'
/usr/lib/ruby/gems/1.8/gems/mongrel-1.0.1/lib/mongrel.rb:720:in `run'
/usr/lib/ruby/gems/1.8/gems/mongrel-1.0.1/lib/mongrel/configurator.rb:271:in `run'
/usr/lib/ruby/gems/1.8/gems/mongrel-1.0.1/lib/mongrel/configurator.rb:270:in `each'
/usr/lib/ruby/gems/1.8/gems/mongrel-1.0.1/lib/mongrel/configurator.rb:270:in `run'
/usr/lib/ruby/gems/1.8/gems/mongrel-1.0.1/bin/mongrel_rails:127:in `run'
/usr/lib/ruby/gems/1.8/gems/mongrel-1.0.1/lib/mongrel/command.rb:211:in `run'
/usr/lib/ruby/gems/1.8/gems/mongrel-1.0.1/bin/mongrel_rails:243
/usr/lib/ruby/gems/1.8/gems/activesupport-1.4.2/lib/active_support/dependencies.rb:488:in `load'
/usr/lib/ruby/gems/1.8/gems/activesupport-1.4.2/lib/active_support/dependencies.rb:488:in `load'
/usr/lib/ruby/gems/1.8/gems/activesupport-1.4.2/lib/active_support/dependencies.rb:342:in `new_constants_in'
/usr/lib/ruby/gems/1.8/gems/activesupport-1.4.2/lib/active_support/dependencies.rb:488:in `load'
/usr/lib/ruby/gems/1.8/gems/rails-1.2.3/lib/commands/servers/mongrel.rb:60
/usr/local/lib/site_ruby/1.8/rubygems/custom_require.rb:27:in `gem_original_require'
/usr/local/lib/site_ruby/1.8/rubygems/custom_require.rb:27:in `require'
/usr/lib/ruby/gems/1.8/gems/activesupport-1.4.2/lib/active_support/dependencies.rb:495:in `require'
/usr/lib/ruby/gems/1.8/gems/activesupport-1.4.2/lib/active_support/dependencies.rb:342:in `new_constants_in'
/usr/lib/ruby/gems/1.8/gems/activesupport-1.4.2/lib/active_support/dependencies.rb:495:in `require'
/usr/lib/ruby/gems/1.8/gems/rails-1.2.3/lib/commands/server.rb:39
/usr/local/lib/site_ruby/1.8/rubygems/custom_require.rb:27:in `gem_original_require'
/usr/local/lib/site_ruby/1.8/rubygems/custom_require.rb:27:in `require'
./script/server:3
-e:4:in `load'
-e:4
-

Jereme Claussen October 10th, 2007 @ 05:21 PM
it looks like habtm uses the delete_records method located in active_record/associations/has_and_belongs_to_many_association.rb to clear out its associations.
I tried adding _records to the option array, but that has no effect.
-

Jereme Claussen October 10th, 2007 @ 05:37 PM
fixed by setting on to:
:on => [:create, :save, :destroy, :destroy_all, :attributes=]
please confirm if this is best method
-

Jereme Claussen October 10th, 2007 @ 06:02 PM
My fix works if I am doing something like internal_user.internal_permissions = [permission1, permission2, etc].
I still fail if I do internal_user.internal_permission_ids = [1,2,3]
The stack trace is:
/usr/lib/ruby/gems/1.8/gems/activerecord-1.15.3/lib/active_record/connection_adapters/abstract_adapter.rb:128:in `log'
/usr/lib/ruby/gems/1.8/gems/activerecord-1.15.3/lib/active_record/connection_adapters/mysql_adapter.rb:243:in `execute'
/usr/lib/ruby/gems/1.8/gems/activerecord-1.15.3/lib/active_record/associations/has_and_belongs_to_many_association.rb:132:in `insert_record'
/usr/lib/ruby/gems/1.8/gems/activerecord-1.15.3/lib/active_record/associations/association_collection.rb:26:in `concat'
/usr/lib/ruby/gems/1.8/gems/activerecord-1.15.3/lib/active_record/associations/association_collection.rb:23:in `each'
/usr/lib/ruby/gems/1.8/gems/activerecord-1.15.3/lib/active_record/associations/association_collection.rb:23:in `concat'
/usr/lib/ruby/gems/1.8/gems/activerecord-1.15.3/lib/active_record/connection_adapters/abstract/database_statements.rb:59:in `transaction'
/usr/lib/ruby/gems/1.8/gems/activerecord-1.15.3/lib/active_record/transactions.rb:95:in `transaction'
/usr/lib/ruby/gems/1.8/gems/activerecord-1.15.3/lib/active_record/transactions.rb:121:in `transaction'
/usr/lib/ruby/gems/1.8/gems/activerecord-1.15.3/lib/active_record/associations/association_collection.rb:22:in `concat'
/usr/lib/ruby/gems/1.8/gems/activerecord-1.15.3/lib/active_record/associations/association_collection.rb:145:in `replace'
/usr/lib/ruby/gems/1.8/gems/activerecord-1.15.3/lib/active_record/connection_adapters/abstract/database_statements.rb:59:in `transaction'
/usr/lib/ruby/gems/1.8/gems/activerecord-1.15.3/lib/active_record/transactions.rb:95:in `transaction'
/usr/lib/ruby/gems/1.8/gems/activerecord-1.15.3/lib/active_record/transactions.rb:121:in `transaction'
/usr/lib/ruby/gems/1.8/gems/activerecord-1.15.3/lib/active_record/associations/association_collection.rb:143:in `replace'
/usr/lib/ruby/gems/1.8/gems/activerecord-1.15.3/lib/active_record/associations.rb:950:in `tags='
/usr/lib/ruby/gems/1.8/gems/activerecord-1.15.3/lib/active_record/associations.rb:960:in `send'
/usr/lib/ruby/gems/1.8/gems/activerecord-1.15.3/lib/active_record/associations.rb:960:in `tag_ids='
app/controllers/admin/tags_controller.rb:44:in `tags_by_customer'
-e:4:in `load'
-e:4
-

Jereme Claussen October 15th, 2007 @ 12:35 PM
The code that generates these collection accessors is:
def collection_accessor_methods(reflection, association_proxy_class) collection_reader_method(reflection, association_proxy_class) define_method("#{reflection.name}=") do |new_value| # Loads proxy class instance (defined in collection_reader_method) if not already loaded association = send(reflection.name) association.replace(new_value) association end define_method("#{reflection.name.to_s.singularize}_ids") do send(reflection.name).map(&:id) end define_method("#{reflection.name.to_s.singularize}_ids=") do |new_value| ids = (new_value || []).reject { |nid| nid.blank? } send("#{reflection.name}=", reflection.class_name.constantize.find(ids)) end endSince the accessor method is dynamically generated, it might be tricky to step in here. I'm going to try a few things and will let you know what I arrive at.
-

Jereme Claussen October 15th, 2007 @ 12:36 PM
That previous code bit, by the way, is located in active_record/associations.rb
-

Jereme Claussen October 15th, 2007 @ 03:23 PM
No offense intended, but I needed a solution right away. I decided to use acts_as_readonlyable as it takes a more failsafe approach of switching connections when you need to read. This way, the worst case scenario is that you occasionally read from the master, as opposed to the ActiveDelegate worst-case where you can't write at all and raise an exception.
I hope you find a way to make this pass all tests and become a solid plugin.
Thanks,
jereme
Please Login or create a free account to add a new comment.
You can update this ticket by sending an email to from your email client. (help)
Create your profile
Help contribute to this project by taking a few moments to create your personal profile. Create your profile »
