Habtm update attribute question

given the following habtm relationship

users

id
name

articles

id
title

articles_users

article_id
user_id
last_read
primary key (article_id, user_id)

class User < ActiveRecord::Base
has_and_belongs_to_many :articles

def read_article(article)
articles.push_with_attributes(article, :last_read => Time.now)
end
end

class Article < ActiveRecord::Base
has_and_belongs_to_many :users
end

i understand that push_with_attributes will create the entry in the join
table, and will also put the current time in the join table.

but what if i need to update the time? ie, i want to track not only that
the
article was read, but WHEN it was last read? would i still use
push_with_attributes or is there another mechanism i am not aware of
that I
can use to update the last_read column in the join table?

Chris

On 11/15/05, Chris H. [email protected] wrote:

but what if i need to update the time? ie, i want to track not only that
the article was read, but WHEN it was last read? would i still use
push_with_attributes or is there another mechanism i am not aware of that I
can use to update the last_read column in the join table?

The path of least resistence is to change the relationship into a
model object. You would replace the HABTM with a has_many and create a
new “Reading” model object.

Hope that helps,


Cheers,

Peter D.

RealityForge.org: http://www.realityforge.org/

I had the same problem. I solved it by adding an “update_attributes”
method
to HABTM. I added this code to application_helper.rb:

module ActiveRecord
module Associations
class HasAndBelongsToManyAssociation

def update_attributes(record, join_attributes = {})

Did they pass in an ID or an object?

if record.is_a? ActiveRecord::Base

Check the type of the passed in record

raise_on_type_mismatch(record)

Find the actual record in @target, if @target is loaded

if loaded?
record_in_arr = @target.find { | item | item == record }
raise ActiveRecord::RecordNotFound, “#{record.class}
#{record.idhttp://record.id}
not found in collection” unless !record_in_arr.nil?

record = record_in_arr
record_id = record.id http://record.id
else
record_id = record.id http://record.id
record = nil
end
else

The record isn’t an ActiveRecord, assume it’s an ID

record_id = record.to_i

If the target is loaded, find the record in the target

if loaded?

Find the actual record in @target

record_in_arr = @target.find { | item | item.id http://item.id ==
record_id }
raise ActiveRecord::RecordNotFound, “Item with ID #{record_id} not found
in
collection” if record_in_arr.nil?

record = record_in_arr
else

Not loaded- for performance, don’t load it, just do an update based on

the
ID
record = nil
end
end

Break the join_attributes into columns and values for those columns

cols_and_vals = join_attributes.to_a.transpose

Join the columns together with ’ = ?, ', so the result for [a, b]

would be ‘a = ?, b’ NOTE: We will have to add a trailing ’ = ?’

in the SQL

col_string = cols_and_vals[0].join(’ = ?, ')

#NOTE: :before_update doesn’t do anything right now- this is “future
proofing”
callback(:before_update, record)

Do the SQL, passing in the args

ret = @owner.connection().update(sanitize_sql([“UPDATE #{@join_table}
SET
#{col_string} = ? WHERE #{@association_class_primary_key_name} = ? AND
#{@association_foreign_key} = ?”, cols_and_vals[1],
@owner.idhttp://owner.id,
record_id].flatten), “Update Attributes”)

Fix up @target, the array of items IF it’s loaded

join_attributes.each { | att, att_val | record[att] = att_val } if
!record.nil?

#NOTE: :after_update doesn’t do anything right now- this is “future
proofing”
callback(:after_update, record)

return ret
end
end
end

class ActiveRecord::Base
#MES- CUSTOMIZATION: Making it so that HABTM can support before_update
and
after_update callbacks

FROM active_record\associations.rb

def self.has_and_belongs_to_many(association_id, options = {},
&extension)
#MES- Note that the ONLY difference between this function and

ActiveRecord::Associations::ClassMethods is that the validation

supports

the :before_update, and :after_update option

options.assert_valid_keys(
:class_name, :table_name, :foreign_key, :association_foreign_key,
:conditions, :include,
:join_table, :finder_sql, :delete_sql, :insert_sql, :order, :uniq,
:before_add, :after_add,
:before_remove, :after_remove, :extend, :before_update, :after_update
)

options[:extend] = create_extension_module(association_id, extension) if
block_given?

association_name, association_class_name,
association_class_primary_key_name

associate_identification(association_id, options[:class_name],
options[:foreign_key])

require_association_class(association_class_name)

options[:join_table] ||=
join_table_name(undecorated_table_name(self.to_s),
undecorated_table_name(association_class_name))

add_multiple_associated_save_callbacks(association_name)

collection_accessor_methods(association_name, association_class_name,
association_class_primary_key_name, options,
HasAndBelongsToManyAssociation)

Don’t use a before_destroy callback since users’ before_destroy

callbacks will be executed after the association is wiped out.

old_method = “destroy_without_habtm_shim_for_#{association_name}”
class_eval <<-end_eval
alias_method :#{old_method}, :destroy_without_callbacks
def destroy_without_callbacks
#{association_name}.clear
#{old_method}
end
end_eval

add_association_callbacks(association_name, options)

deprecated api

deprecated_collection_count_method(association_name)
deprecated_add_association_relation(association_name)
deprecated_remove_association_relation(association_name)
deprecated_has_collection_method(association_name)
end

def self.add_association_callbacks(association_name, options)
#MES- Note that the ONLY difference between this function and the
original
is the

addition of the before_update and after_update callbacks

#callbacks = %w(before_add after_add before_remove after_remove)
callbacks = %w(before_add after_add before_remove after_remove
before_update
after_update)
callbacks.each do |callback_name|
full_callback_name =
“#{callback_name.to_s}for#{association_name.to_s}”
defined_callbacks = options[callback_name.to_sym]
if options.has_key?(callback_name.to_sym)
class_inheritable_reader full_callback_name.to_sym
write_inheritable_array(full_callback_name.to_sym,
[defined_callbacks].flatten)
end
end
end
end

Would it be possible to give an example of how to call this method,
which parameters should be passed to it and using which syntax? Thanks!

smedberg wrote:

I had the same problem. I solved it by adding an “update_attributes”
method
to HABTM. I added this code to application_helper.rb:

module ActiveRecord
module Associations
class HasAndBelongsToManyAssociation

def update_attributes(record, join_attributes = {})