I have the following script which demonstrates me that has_many roles=
attribute always works in a persistent manner.
My questions are:
1) What is the reason behind this behavior: why has_many attributes are persisted right at the moment when they've been set? Why this difference from regular attributes behavior (name
in the following script) ?
2) Can I write my custom roles=
setter so I could use fx assign_attributes
for a bunch of models attributes (including roles=) without roles association to be persisted? I would appreciate an example if it is possible in Rails > 3.2 ?
Here is the script:
gem 'rails', '>=3.2.0' # change as required
gem 'sqlite3'
require 'active_record'
require 'logger'
puts "Active Record #{ActiveRecord::VERSION::STRING}"
ActiveRecord::Base.logger = Logger.new(STDERR)
ActiveRecord::Base.establish_connection(
:adapter => 'sqlite3',
:database => ':memory:'
)
ActiveRecord::Schema.define do
create_table :users, :force => true do |t|
t.string :name
end
create_table :user_roles, :force => true do |t|
t.integer :user_id
t.integer :role_id
end
create_table :roles, :force => true do |t|
t.string :name
end
end
# Create the minimal set of models to reproduce the bug
class User < ActiveRecord::Base
has_many :user_roles
has_many :roles, :through => :user_roles
end
class UserRole < ActiveRecord::Base
belongs_to :user
belongs_to :role
end
class Role < ActiveRecord::Base
end
r = Role.create(:name => 'admin')
u = User.create
# roles= persists its value, name= does not
u.assign_attributes({ :roles => [r], :name => 'Stanislaw' })
# The same behavior is produced by:
# u.attributes=
# u.roles=
puts "name attribute: #{u.name}"
puts "many roles #{u.roles}"
u.reload
puts "name attribute: #{u.name}"
puts "many roles #{u.roles}" # I see admin role and I want to achieve behavior that I would not see it
Associations are not the same as attributes. For example with a has_many association all you are doing when you assign is setting the foreign key on the belongs_to side.
class User < ActiveRecord::Base
has_many :posts
end
class Post < ActiveRecord::Base
belongs_to :user
end
p = Post.create
u = User.create
u.posts << p # this line will simply update p.user_id with u.id
In your example with the join table assigning a role to a user will create a UserRole record and with the user_id/role_id records set. This happens because you declared the has_many :through
As for preventing this behavior, you could use a virtual attribute that stores the unpersisted roles until you save the record, then create the associations.
class User < ActiveRecord::Base
attr_accessor :unpersisted_roles
attr_accessible :unpersisted_roles
after_save :assign_roles
def assign_roles
self.roles << @unpersisted_roles if defined(@unpersisted_roles)
end
end
r = Role.create
u = User.create
u.attributes = {:unpersisted_roles => [r]}
u.save # roles get persisted here
This is only a simple example, actual code might need to be more complicated or require diving deeper into AR's interface to get it working without too many side effects.
If you could give some insight as to why your wanting to not persist the association I might be able to suggest a more specific course of action.
Update
In reference to Issue #3 with some comments where changes were made.
module SimpleRoles
module Many
module Persistence
class << self
def included base
base.class_eval %{
has_many :user_roles
has_many :roles, :through => :user_roles
# Add a callback to persist the roles
after_create :persist_roles
}
end
end
def roles
# Apply unpersisted roles in case we want to access them before saving
super.map(&:name).map(&:to_sym) + (@unpersisted_roles || [])
end
def roles= *rolez
rolez.to_symbols!.flatten!
# if we're already persisted then go ahead and save
# otherwise stash them in an ivar array
if persisted?
super retrieve_roles(rolez)
else
@unpersisted_roles = rolez
end
end
private
# Our callback method that sets the roles, this will
# work since persisted? is true when this runs.
def persist_roles
self.roles = @unpersisted_roles
end
def retrieve_roles rolez
raise "Not a valid role!" if (rolez - config.valid_roles).size > 0
rolez.map do |rolle|
begin
Role.find_by_name! rolle.to_s
rescue
raise "Couldn't find Role for #{rolle}. Maybe you need to re-run migrations?"
end
end
end
def config
SimpleRoles::Configuration
end
end
end
end