Rails4 : How to assign a nested resource id to another resource - ruby-on-rails

Model:
order & material
order has_many materials
material belongs_to order
material & user
material has_may users
user belongs_to material
Assume I create a material with id = 20 , order_id = 1
In materials_controller update action, I want to assign material id to specific users.In materials_controller update action I did it like this
if #material.update_attributes(material_params)
if #material.ready == true
#users = User.where("is_manager = 't'")
#users.each do |user|
user.material_id = #material.id
end
end
end
But attribute material_id in user did not get changed after the action.
Anybody could tell me what cause the failure to pass material id to user ?

You also need to do user.save after you change user.material_id.
user.material_id = #material.id
user.save #Saves the changes
That changed the attribute of user object, but the change has not been persisted yet. It's now a stale object which has some attributes changed. To persist those attributes, user.save is required.
Or you can use update_attribute like below:
if #material.update_attributes(material_params)
if #material.ready
#users = User.where("is_manager = 't'")
#users.each do |user|
user.update_attribute(:material_id, #material.id)
end
end
end

You might want to have a look at update_all.
update_all updates all records in one SQL statement instead of loading all records into memory and sending N update queries to the database. This makes update_all much faster than iterating over multiple users.
if #material.ready
User.where(is_manager: 't').update_all(material_id: #material.id)
end
Often it is an issue, that update_all doesn't validate the records before updating. But in this case this behavior is actually preferred.

Related

Reassociate all related models in rails

Ok, We f&^%$**&ed up.
We lost a bunch of user records. At some point, an integration file ran which re-inserted some of the lost records.
The problem is that the new users have a different ID than the original user, so all the existing related content for the old User id has been orphaned. I now need to go back in and reassociate all the orphaned stuff to the new User id. It won't be enough to simply give the new user the old Id from backup, because there will be new content associated to the new User Id.
We know the reflect_on_all_associations method, but that is hard to use for finding stuff. However, this could be a starting point for a script of some kind.
Any clues on how to have a method return all models related to a particular model based on associations, without having to specify or know those associations?
Here's a way to use reflect_all_associations: You can iterate through the associations, select only the has_many and has_one, and then update those records. Here's a helper class to do the job which you can execute by calling AssociationFixer.new(user_to_destroy, original_user_id).fix_associations:
class AssociationFixer
USER_ASSOCIATIONS = User.reflect_on_all_associations
def initialize(user_to_destroy, original_user_id)
#user_to_destroy = user_to_destroy
#original_user_id = original_user_id
end
def fix_associations
USER_ASSOCIATIONS.each do |association|
next if association.options.has_key? :through
if association.macro == :has_many
fix_has_many(association)
elsif association.macro == :has_one
fix_has_one(association)
end
end
end
def fix_has_many(association)
#user_to_destroy.send(association.name).each do |record|
if association.options.has_key? :foreign_key
record.send(assignment_method(association.foreign_key), #original_user_id)
else
record.user_id = #original_user_id
end
record.save
end
end
def fix_has_one(association)
if association.options.has_key? :foreign_key
#user_to_destroy.send(association.name).send(assignment_method(association.foreign_key), #original_user_id)
else
#user_to_destroy.send(assignment_method(association.name.user_id), #original_user_id)
end
record.save
end
def assigment_method(method_name)
method_name.to_s + '='
end
end
This sounds like a problem for SQL. I would look at importing your backup tables into the database in a separate table, if it's possible to join them on a column, maybe email or user_name or something. Then you can run a select based on the old id and update to the new id. Best of luck!

Rails Counter Cache

I have a db schema with multiple counter caches. Rather than have the counter caches spread across many tables, I'd like to have a table called user_counters with all of the various counter caches. I'd update these caches from a service object with a method like this:
def update_user_counter(column, user_id)
UserCounter.increment_counter(column, user_id)
end
But, the UserCounter record for the associated user will most likely not have the same ID as the user. What I'd like to do is something like this:
def update_user_counter(column, user_id)
UserCounter.increment_counter(column, UserCounter.where('user_id = "#{user_id}"')
end
Any thoughts as to how I can accomplish this? Thanks!!
If these are 10 attributes associated with a User, is there any reason to not simply make them a part of the User model? If that is the case, the below code will still work (just make the method part of User instead of UserCounter).
If you want to implement it your way, I'd say first make sure that your user_counters table has an index on user_id (since Rails is dumb about adding indexes and FK's).
Then, you could do this (passing in "column" as a symbol):
class UserCounter < ActiveRecord::Base
#other code
def self.update_user_counter(column, user_id)
counter = UserCounter.find_by_user_id(user_id)
counter.update_attribute(column, counter.read_attribute(column) + 1)
counter.save!
end
end
And in your other model:
class SomeOtherModel < ActiveRecord::Base
#Model code
def update_counter(user)
UserCounter.update_user_counter(:this_column, user.id)
end
end
This solution (or using the update_counter method) both require two database hits, one for the lookup and one for the update. If you need the speed, you could write this directly in SQL:
query = "UPDATE user_counters SET #{column} = #{column} + 1 WHERE user_id = #{user_id};"
ActiveRecord::Base.connection.execute(query);
This assumes that user_id is unique on user_counters.

Rails 3: Join Table with additional attributes

I have a simple cart system that I have been working on for a little while for an application and am needing a little help in trying to figure out how to update a particular attribute in a join table (Between Order and Products).
Here is the code:
def add_product_to_cart
#product = Product.by_client(current_client).first
#order = current_order
unless #order.products.exists? :id => #product.id
#order.products << #product
end
end
I am trying to update a particular attribute when I update the #order.products...
This is what I am trying to do:
#order.products << #product --> When this happens I need to update a :price attribute..
Anyway of doing this?
class Order
has_many :products
def price
products.sum(:price)
end
end
Just off the top of my head. Here's the sum reference:
http://ar.rubyonrails.org/classes/ActiveRecord/Calculations/ClassMethods.html#M000296
Desire to put attributes into join table may be a sign of missing model. You can promote join table into model, say OrderItem, by adding primary key to it. HABTM associations in Order and Product then become has_many through associations. The new model would be a good place for setting up callback which populates price attribute. It can also unlock additional benefits, like time-stamping items and making them act_as_list, etc.

before_create still saves

Before everything i would like to thank you for your help
I have a model like this:
attr_protected nil
belongs_to :product
belongs_to :user
before_create :add_ammount
def carted_product_price(ammount, price)
ammount * price
end
def add_ammount
carted_product = CartedProduct.where(:product_id => self.product_id, :user_id => self.user_id)
if carted_product
carted_product.first.ammount += self.ammount
carted_product.first.update_attributes(:ammount => carted_product.first.ammount)
else
self.save
end
end
it saves buying orders in a table called Carted_Products connected to Users and Products in the belogings
the problem is that when the Before create executes i want it to update the record in the table adding the ammount passed by the controller if the record already exists and if not, create one, as far as iv done, it updates the ammount but STILL CREATES A NEW one with the passed params in the order, i want it only to update, not to do both actions when the record is found
thnx for your patience
EDIT:
Tried returning false after the update attributes, it cancels the filter, and dont create or update attributes
Return false in the before_create filter to prevent the object form being saved. add_amount is not responsible for saving the object, and shouldn't call save by itself.
You cannot do this in before_create filter. You need to fetch existing CartedProduct in controller where you're calling create.

Use rails nested model to *create* outer object and simultaneously *edit* existing nested object?

Using Rails 2.3.8
Goal is to create a Blogger while simultaneously updating the nested User model (in case info has changed, etc.), OR create a brand new user if it doesn't exist yet.
Model:
class Blogger < ActiveRecord::Base
belongs_to :user
accepts_nested_attributes_for :user
end
Blogger controller:
def new
#blogger = Blogger.new
if user = self.get_user_from_session
#blogger.user = user
else
#blogger.build_user
end
# get_user_from_session returns existing user
# saved in session (if there is one)
end
def create
#blogger = Blogger.new(params[:blogger])
# ...
end
Form:
<% form_for(#blogger) do |blogger_form| %>
<% blogger_form.fields_for :user do |user_form| %>
<%= user_form.label :first_name %>
<%= user_form.text_field :first_name %>
# ... other fields for user
<% end %>
# ... other fields for blogger
<% end %>
Works fine when I'm creating a new user via the nested model, but fails if the nested user already exists and has and ID (in which case I'd like it to simply update that user).
Error:
Couldn't find User with ID=7 for Blogger with ID=
This SO question deals with a similar issue, and only answer suggests that Rails simply won't work that way. The answer suggests simply passing the ID of the existing item rather than showing the form for it -- which works fine, except I'd like to allow edits to the User attributes if there are any.
Deeply nested Rails forms using belong_to not working?
Suggestions? This doesn't seem like a particularly uncommon situation, and seems there must be a solution.
I'm using Rails 3.2.8 and running into the exact same problem.
It appears that what you are trying to do (assign/update an existing saved record to a belongs_to association (user) of a new unsaved parent model (Blogger) is simply not possible in Rails 3.2.8 (or Rails 2.3.8, for that matter, though I hope you've upgraded to 3.x by now)... not without some workarounds.
I found 2 workarounds that appear to work (in Rails 3.2.8). To understand why they work, you should first understand the code where it was raising the error.
Understanding why ActiveRecord is raising the error...
In my version of activerecord (3.2.8), the code that handles assigning nested attributes for a belongs_to association can be found in lib/active_record/nested_attributes.rb:332 and looks like this:
def assign_nested_attributes_for_one_to_one_association(association_name, attributes, assignment_opts = {})
options = self.nested_attributes_options[association_name]
attributes = attributes.with_indifferent_access
if (options[:update_only] || !attributes['id'].blank?) && (record = send(association_name)) &&
(options[:update_only] || record.id.to_s == attributes['id'].to_s)
assign_to_or_mark_for_destruction(record, attributes, options[:allow_destroy], assignment_opts) unless call_reject_if(association_name, attributes)
elsif attributes['id'].present? && !assignment_opts[:without_protection]
raise_nested_attributes_record_not_found(association_name, attributes['id'])
elsif !reject_new_record?(association_name, attributes)
method = "build_#{association_name}"
if respond_to?(method)
send(method, attributes.except(*unassignable_keys(assignment_opts)), assignment_opts)
else
raise ArgumentError, "Cannot build association #{association_name}. Are you trying to build a polymorphic one-to-one association?"
end
end
end
In the if statement, if it sees that you passed a user ID (!attributes['id'].blank?), it tries to get the existing user record from the blogger's user association (record = send(association_name) where association_name is :user).
But since this is a newly built Blogger object, blogger.user is going to initially be nil, so it won't get to the assign_to_or_mark_for_destruction call in that branch that handles updating the existing record. This is what we need to work around (see the next section).
So it moves on to the 1st else if branch, which again checks if a user ID is present (attributes['id'].present?). It is present, so it checks the next condition, which is !assignment_opts[:without_protection].
Since you are initializing your new Blogger object with Blogger.new(params[:blogger]) (that is, without passing as: :role or without_protection: true), it uses the default assignment_opts of {}. !{}[:without_protection] is true, so it proceeds to raise_nested_attributes_record_not_found, which is the error that you saw.
Finally, if neither of the other 2 if branches were taken, it checks if it should reject the new record and (if not) proceeds to build a new record. This is the path it follows in the "create a brand new user if it doesn't exist yet" case you mentioned.
Workaround 1 (not recommended): without_protection: true
The first workaround I thought of -- but wouldn't recommend -- was be to assign the attributes to the Blogger object using without_protection: true (Rails 3.2.8).
Blogger.new(params[:blogger], without_protection: true)
This way it skips the 1st elsif and goes to the last elsif, which builds up a new user with all the attributes from the params, including :id. Actually, I don't know if that will cause it to update the existing user record like you were wanting (probably not—haven't really tested that option much), but at least it avoids the error... :)
Workaround 2 (recommended): set self.user in user_attributes=
But the workaround that I would recommend more than that is to actually initialize/set the user association from the :id param so that the first if branch is used and it updates the existing record in memory like you want...
accepts_nested_attributes_for :user
def user_attributes=(attributes)
if attributes['id'].present?
self.user = User.find(attributes['id'])
end
super
end
In order to be able to override the nested attributes accessor like that and call super, you'll need to either be using edge Rails or include the monkey patch that I posted at https://github.com/rails/rails/pull/2945. Alternatively, you can just call assign_nested_attributes_for_one_to_one_association(:user, attributes) directly from your user_attributes= setter instead of calling super.
If you want to make it always create a new user record and not update existing user...
In my case, I ended up deciding that I didn't want people to be able to update existing user records from this form, so I ended up using a slight variation of the workaround above:
accepts_nested_attributes_for :user
def user_attributes=(attributes)
if user.nil? && attributes['id'].present?
attributes.delete('id')
end
super
end
This approach also prevents the error from occurring, but does so a little differently.
If an id is passed in the params, instead of using it to initialize the user association, I just delete the passed-in id so that it will fall back to building a new user from the rest of the submitted user params.
I ran into the same error in rails 3.2. The error occurred when using a nested form to create a new object with a belongs to relationship for an existing object. Tyler Rick's approach did not work for me. What I found to work was to set the relationship following the initialization of the object and then setting the objects attributes. An example of this is as follows ...
#report = Report.new()
#report.user = current_user
#report.attributes = params[:report]
assuming params looks something like ...
{:report => { :name => "name", :user_attributes => {:id => 1, { :things_attributes => { "1" => {:name => "thing name" }}}}}}
Try adding a hidden field for the user's id in the nested form:
<%=user_form.hidden_field :id%>
The nested save will use this to determine if it is a create or an update for the User.

Resources