I have a models like Routine and RoutineContent for localization
in Routine.rb
Class Routine < ActiveRecord::Base
has_many :routine_contents, dependent: :destroy
accepts_nested_attributes_for :routine_contents, reject_if: proc {|attributes| attributes['title'].empty?}
end
and in RoutinesContent
class RoutineContent < ActiveRecord::Base
belongs_to :routine
validates_presence_of :title
end
In the new Routine action I puts on RoutineConten fields for languages. If title in one object is emty then this object will rejected.
When I go to edit action, I do this
def set_routine_contents
contents = #routine.routine_contents.group_by {|content| content.lang}
if contents['ru'].nil?
#routine.routine_contents << RoutineContent.new(lang: 'ru')
end
if contents['en'].nil?
#routine.routine_contents << RoutineContent.new(lang: 'en')
end
end
end after this Rails INSERT INTO emty object in table, why? How I can disable it?
Thanks
Solution
def set_routine_contents
contents = #routine.routine_contents.group_by {|content| content.lang}
if contents['ru'].nil?
#routine.routine_contents.build(lang: 'ru')
end
if contents['en'].nil?
#routine.routine_contents.build(lang: 'en')
end
end
Use the build method. Add to Array via << it was bad idea
has_many association implemented with foreign key in routine_id in routine_contents table.
So adding new RoutineContent to your Routine requires determined primary key in Routine to write to routine_id, and causes Routine to save if not saved yet.
Related
An application I'm working on, is trying to use the concept of polymorphism without using polymorphism.
class User
has_many :notes
end
class Customer
has_many :notes
end
class Note
belongs_to :user
belongs_to :customer
end
Inherently we have two columns on notes: user_id and customer_id, now the bad thing here is it's possible for a note to now have a customer_id and a user_id at the same time, which I don't want.
I know a simple/better approach out of this is to make the notes table polymorphic, but there are some restrictions, preventing me from doing that right now.
I'd like to know if there are some custom ways of overriding these associations to ensure that when one is assigned, the other is unassigned.
Here are the ones I've tried:
def user_id=(id)
super
write_attribute('customer_id', nil)
end
def customer_id=(id)
super
write_attribute('user_id', nil)
end
This doesn't work when using:
note.customer=customer or
note.update(customer: customer)
but works when using:
note.update(customer_id: 12)
I basically need one that would work for both cases, without having to write 4 methods:
def user_id=(id)
end
def customer_id=(id)
end
def customer=(id)
end
def user=(id)
end
I would rather use ActiveRecord callbacks to achieve such results.
class Note
belongs_to :user
belongs_to :customer
before_save :correct_assignment
# ... your code ...
private
def correct_assignment
if user_changed?
self.customer = nil
elsif customer_changed?
self.user = nil
end
end
end
Say I have a simple model like this with a field called "name" and an attribute called "aliased_name":
class User < ActiveRecord::Base
attr_accessor :aliased_name
end
User.create(name: "Faye Kname")
I can do:
user=User.select(:id, :name)
user.name # Faye Kname
But how can I use select to populate the aliased_name attribute.
user=User.select(:id, "name AS aliased_name")
user.aliased_name # nil
user[:aliased_name] # Faye Kname
I can access on the :aliased_name symbol, but the attribute is not assigned. I'd like to not have to do
user.aliased_name = user[:aliased_name]
I'm actually doing a more complex join on another table and I'm trying to select a field from the join table into the alias, but figured this would be a simpler example.
Typically I do these kinds of aliases with methods instead of attr_accessors. Something like
def aliased_name
has_attribute?(:aliased_name) ? read_attribute(:aliased_name) : self.name
end
The has_attribute? is there in case you didn't load the attribute with your query, so you can have a default value.
So the attr_accessor is looking for the instance variable #aliased_name which I don't think is being set in your code. You can set it with #aliased_name = "some value" or using the attr_accessor aliased_name = "some value", but it's not going to be set with the initial query that returns the object, or in the second SELECT query, at least as it's written now.
One route that might make sense would be to use both a separate method and attr_writer. Something like this
attr_writer :aliased_name
def aliased_name
#aliased_name ||= self.name
end
This sets the instance variable the first time it's called and leaves you free to change it with the attr_writer. I'm not sure how this fits in with the more complex join, but this is a fairly simple way to solve the problem you describe initially.
You may be better using alias_attribute:
#app/models/user.rb
class User < ActiveRecord::Base
alias_attribute :aliased_name, :name
end
Although it will only take user.name data & put it into user.alias_attribute
I'm trying to select a field from the join table into the alias
Done this before:
Rails Scoping For has_many :through To Access Extra Data
Accessing additional values on has_many through Rails
You have two options. Either use an SQL ALIAS column, or access the proxy_association method in your model. I have worked extensively with both:
--
SQL Alias
#app/models/parent.rb
class Parent < ActiveRecord::Base
has_many :joins
has_many :children, -> { select("#{Parent.table_name}.*, #{Join.table_name}.attr AS alias_name") }, through: :joins, dependent: :destroy
end
This will give you...
#parent.children.each do |child|
child.alias_name
end
--
Association Extensions
The next method is a lot more complicated; more efficient:
#app/models/parent.rb
class Parent < ActiveRecord::Base
has_many :joins
has_many :children, through: :joins, -> { extending AliasAttribute }
end
#app/models/concerns/alias_attribute.rb
module PlayerPermission
#Load
def load
alias_names.each do |permission|
proxy_association.target << permission
end
end
#Private
private
#Names
def names
return_array = []
through_collection.each_with_index do |through,i|
associate = through.send(reflection_name)
associate.assign_attributes({name: items[i]})
return_array.concat Array.new(1).fill( associate )
end
return_array
end
#######################
# Variables #
#######################
#Association
def reflection_name
proxy_association.source_reflection.name
end
#Foreign Key
def through_source_key
proxy_association.reflection.source_reflection.foreign_key
end
#Primary Key
def through_primary_key
proxy_association.reflection.through_reflection.active_record_primary_key
end
#Through Name
def through_name
proxy_association.reflection.through_reflection.name
end
#Through
def through_collection
proxy_association.owner.send through_name
end
#Captions
def items
through_collection.map(&:name)
end
#Target
def target_collection
#load_target
proxy_association.target
end
end
Each time you call an association, you have access to the .association object for it. Within the association itself, you have access to proxy_association objects; all of which can be manipulated to insert the aliased data into your parent data.
The above will allow you to use:
#parent = Parent.find x
#parent.children.each do |child|
child.alias_name
end
I can provide support if required.
I have a form for creating a new invoice with many items.
class Invoice < ActiveRecord::Base
attr_accessible :project_id, :number, :date, :recipient, :items_attributes
accepts_nested_attributes_for :items
end
Now when I instantiate a new invoice and a set of containing items, I want these items to know something about the invoice they belong to even before they are saved, so I can do something like this in my Item model:
class Item < ActiveRecord::Base
belongs_to :invoice
after_initialize :set_hourly_rate
private
def set_hourly_rate
if new_record?
self.price ||= invoice.project.hourly_rate
end
end
end
Right now, my code fails because the child (item) doesn't know anything about its parent (invoice) during instantiation. Only after saving the invoice (and thus its nested items), it all works out. But I want to set a default value on each new item even before it gets saved.
How can this be done?
Thanks for any help.
You can add a callback on the invoice association, as follows:
class Invoice < ActiveRecord::Base
# Code
belongs_to :project
has_many :items, :after_add => :set_item_price
private
def set_item_price(item)
item.price = project.hourly_rate
end
end
Once you have your invoice object, you can create children records with the .items.build method (docs here)
items created through this method should have a reference to the invoice
Though, I think they will have the reference only if the Invoice has been persisted (not really sure about that.)
on a Ruby on Rails project I'm trying to access association objects on an ActiveRecord prior to saving everything to the database.
class Purchase < ActiveRecord::Base
has_many :purchase_items, dependent: :destroy
has_many :items, through: :purchase_items
validate :item_validation
def item_ids=(ids)
ids.each do |item_id|
purchase_items.build(item_id: item_id)
end
end
private
def item_validation
items.each do |item|
## Lookup something with the item
if item.check_something
errors.add :base, "Error message"
end
end
end
end
If I build out my object like so:
purchase = Purchase.new(item_ids: [1, 2, 3]) and try to save it the item_validation method doesn't have the items collection populated yet, so even though items have been set set it doesn't get a chance to call the check_something method on any of them.
Is it possible to access the items collection before my purchase model and association models are persisted so that I can run validations against them?
If I change my item_validation method to be:
def item_validation
purchase_items.each do |purchase_item|
item = purchase_item.item
## Lookup something with the item
if item.something
errors.add :base, "Error message"
end
end
end
it seems to work the way I want it to, however I find it hard to believe that there is no way to directly access the items collection with rails prior to my purchase and associated records being saved to the database.
Try to adding the argument inverse_of: in the has_many and belongs_to definitions. The inverse_of argument it's the name of the relation on the other model, For example:
class Post < ActiveRecord::Base
has_many :comments, inverse_of: :post
end
class Comment < ActiveRecord::Base
belongs_to :post, inverse_of: :comments
end
Don't forget to add it also on the other classes, such as PurchaseItem and Item
Hope it helps
Remove your own item_ids= method - rails generates one for you (see collection_singular_ids=ids). This might already solve your problem.
class Purchase < ActiveRecord::Base
has_many :purchase_items, dependent: :destroy
has_many :items, through: :purchase_items
validate :item_validation
private
def item_validation
items.each do |item|
## Lookup something with the item
if item.check_something
errors.add :base, "Error message"
end
end
end
end
The second thing that comes in my mind looking at your code: Move the validation to the Item class. So:
class Purchase < ActiveRecord::Base
has_many :purchase_items, dependent: :destroy
has_many :items, through: :purchase_items
end
class Item < ActiveRecord::Base
has_many :purchase_items
has_many :purchases, through: :purchase_items
validate :item_validation
private
def item_validation
if check_something
errors.add :base, "Error message"
end
end
end
Your Purchase record will also be invalid if one of the Items is invalid.
Do you have documentation that indicates purchase = Purchase.new(item_ids: [1, 2, 3]) does what you're expecting?
To me that looks like you are just setting the non-database attribute 'item_ids' to an array (i.e. not creating an association).
Your Purchase model should not even have any foreign key columns to set directly. Instead there are entries in the purchase_items table that have a purchase_id and item_id. To create a link between your purchase and the three items you need to create three entries in the joiner table.
What happens if you just do this instead?:
purchase = Purchase.new
purchase.items = Item.find([1,2,3])
You can use model.associations = [association_objects] and an Association Callback http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#label-Association+callbacks
I assume you can't access them because id of Purchase isn't available before record saved. But as you mention you have access to first level association purchase_items, so you can extract all ids and pass them in where for Item:
items = Item.where(purchase_item_id: purchase_items.map(&:id))
I'm using Rails' accepts_nested_attributes_for method with great success, but how can I have it not create new records if a record already exists?
By way of example:
Say I've got three models, Team, Membership, and Player, and each team has_many players through memberships, and players can belong to many teams. The Team model might then accept nested attributes for players, but that means that each player submitted through the combined team+player(s) form will be created as a new player record.
How should I go about doing things if I want to only create a new player record this way if there isn't already a player with the same name? If there is a player with the same name, no new player records should be created, but instead the correct player should be found and associated with the new team record.
When you define a hook for autosave associations, the normal code path is skipped and your method is called instead. Thus, you can do this:
class Post < ActiveRecord::Base
belongs_to :author, :autosave => true
accepts_nested_attributes_for :author
# If you need to validate the associated record, you can add a method like this:
# validate_associated_record_for_author
def autosave_associated_records_for_author
# Find or create the author by name
if new_author = Author.find_by_name(author.name)
self.author = new_author
else
self.author.save!
end
end
end
This code is untested, but it should be pretty much what you need.
Don't think of it as adding players to teams, think of it as adding memberships to teams. The form doesn't work with the players directly. The Membership model can have a player_name virtual attribute. Behind the scenes this can either look up a player or create one.
class Membership < ActiveRecord::Base
def player_name
player && player.name
end
def player_name=(name)
self.player = Player.find_or_create_by_name(name) unless name.blank?
end
end
And then just add a player_name text field to any Membership form builder.
<%= f.text_field :player_name %>
This way it is not specific to accepts_nested_attributes_for and can be used in any membership form.
Note: With this technique the Player model is created before validation happens. If you don't want this effect then store the player in an instance variable and then save it in a before_save callback.
A before_validation hook is a good choice: it's a standard mechanism resulting in simpler code than overriding the more obscure autosave_associated_records_for_*.
class Quux < ActiveRecord::Base
has_and_belongs_to_many :foos
accepts_nested_attributes_for :foos, reject_if: ->(object){ object[:value].blank? }
before_validation :find_foos
def find_foos
self.foos = self.foos.map do |object|
Foo.where(value: object.value).first_or_initialize
end
end
end
When using :accepts_nested_attributes_for, submitting the id of an existing record will cause ActiveRecord to update the existing record instead of creating a new record. I'm not sure what your markup is like, but try something roughly like this:
<%= text_field_tag "team[player][name]", current_player.name %>
<%= hidden_field_tag "team[player][id]", current_player.id if current_player %>
The Player name will be updated if the id is supplied, but created otherwise.
The approach of defining autosave_associated_record_for_ method is very interesting. I'll certainly use that! However, consider this simpler solution as well.
Just to round things out in terms of the question (refers to find_or_create), the if block in Francois' answer could be rephrased as:
self.author = Author.find_or_create_by_name(author.name) unless author.name.blank?
self.author.save!
This works great if you have a has_one or belongs_to relationship. But fell short with a has_many or has_many through.
I have a tagging system that utilizes a has_many :through relationship. Neither of the solutions here got me where I needed to go so I came up with a solution that may help others. This has been tested on Rails 3.2.
Setup
Here are a basic version of my Models:
Location Object:
class Location < ActiveRecord::Base
has_many :city_taggables, :as => :city_taggable, :dependent => :destroy
has_many :city_tags, :through => :city_taggables
accepts_nested_attributes_for :city_tags, :reject_if => :all_blank, allow_destroy: true
end
Tag Objects
class CityTaggable < ActiveRecord::Base
belongs_to :city_tag
belongs_to :city_taggable, :polymorphic => true
end
class CityTag < ActiveRecord::Base
has_many :city_taggables, :dependent => :destroy
has_many :ads, :through => :city_taggables
end
Solution
I did indeed override the autosave_associated_recored_for method as follows:
class Location < ActiveRecord::Base
private
def autosave_associated_records_for_city_tags
tags =[]
#For Each Tag
city_tags.each do |tag|
#Destroy Tag if set to _destroy
if tag._destroy
#remove tag from object don't destroy the tag
self.city_tags.delete(tag)
next
end
#Check if the tag we are saving is new (no ID passed)
if tag.new_record?
#Find existing tag or use new tag if not found
tag = CityTag.find_by_label(tag.label) || CityTag.create(label: tag.label)
else
#If tag being saved has an ID then it exists we want to see if the label has changed
#We find the record and compare explicitly, this saves us when we are removing tags.
existing = CityTag.find_by_id(tag.id)
if existing
#Tag labels are different so we want to find or create a new tag (rather than updating the exiting tag label)
if tag.label != existing.label
self.city_tags.delete(tag)
tag = CityTag.find_by_label(tag.label) || CityTag.create(label: tag.label)
end
else
#Looks like we are removing the tag and need to delete it from this object
self.city_tags.delete(tag)
next
end
end
tags << tag
end
#Iterate through tags and add to my Location unless they are already associated.
tags.each do |tag|
unless tag.in? self.city_tags
self.city_tags << tag
end
end
end
The above implementation saves, deletes and changes tags the way I needed when using fields_for in a nested form. I'm open to feedback if there are ways to simplify. It is important to point out that I am explicitly changing tags when the label changes rather than updating the tag label.
Answer by #François Beausoleil is awesome and solved a big problem. Great to learn about the concept of autosave_associated_record_for.
However, I found one corner case in this implementation. In case of update of existing post's author(A1), if a new author name(A2) is passed, it will end up changing the original(A1) author's name.
p = Post.first
p.author #<Author id: 1, name: 'JK Rowling'>
# now edit is triggered, and new author(non existing) is passed(e.g: Cal Newport).
p.author #<Author id: 1, name: 'Cal Newport'>
Oringinal code:
class Post < ActiveRecord::Base
belongs_to :author, :autosave => true
accepts_nested_attributes_for :author
# If you need to validate the associated record, you can add a method like this:
# validate_associated_record_for_author
def autosave_associated_records_for_author
# Find or create the author by name
if new_author = Author.find_by_name(author.name)
self.author = new_author
else
self.author.save!
end
end
end
It is because, in case of edit, self.author for post will already be an author with id:1, it will go in else, block and will update that author instead of creating new one.
I changed the code(elsif condition) to mitigate this issue:
class Post < ActiveRecord::Base
belongs_to :author, :autosave => true
accepts_nested_attributes_for :author
# If you need to validate the associated record, you can add a method like this:
# validate_associated_record_for_author
def autosave_associated_records_for_author
# Find or create the author by name
if new_author = Author.find_by_name(author.name)
self.author = new_author
elsif author && author.persisted? && author.changed?
# New condition: if author is already allocated to post, but is changed, create a new author.
self.author = Author.new(name: author.name)
else
# else create a new author
self.author.save!
end
end
end
#dustin-m's answer was instrumental for me - I am doing something custom with a has_many :through relationship. I have a Topic which has one Trend, which has many children (recursive).
ActiveRecord does not like it when I configure this as a standard has_many :searches, through: trend, source: :children relationship. It retrieves topic.trend and topic.searches but won't do topic.searches.create(name: foo).
So I used the above to construct a custom autosave and am achieving the correct result with accepts_nested_attributes_for :searches, allow_destroy: true
def autosave_associated_records_for_searches
searches.each do | s |
if s._destroy
self.trend.children.delete(s)
elsif s.new_record?
self.trend.children << s
else
s.save
end
end
end