ActiveRecord find_or_build_by - ruby-on-rails

I would like to perform:
XXX.find_or_build_by_language_id(attributes)
I found
XXX.find_or_initialize_by_language_id(attributes)
but that only set language_id and no other attributes. Even if I manually sets the attributes, the record is not saved when I perform XXX.save.
I just read Rails - find or create - is there a find or build?, which seems related to my problem but does not fit my needs.
Edit
Let's use this scenario
# db/migrations/create_models.rb
class CreateModels < ActiveRecord::Migration
def self.up
create_table :companies do |t|
t.string :name
end
create_table :employees do |t|
t.string :name
t.string :city
t.references :company
end
end
end
-
# app/models/employee.rb
class Employee < ActiveRecord::Base
belongs_to :company
end
-
# app/models/company.rb
class Company < ActiveRecord::Base
has_many :employees
end
-
# rails console
:001> c = Company.new
=> #<Company id: nil, name: nil>
:002> c.employees
=> []
:003> e = c.employees.find_or_initialize_by_name(:name => 'foo', :city => 'bar')
=> #<Employee id: nil, name: "foo", city: "bar", company_id: nil>
:004> c.employees
=> []
:005> c.save
=> true
:006> c.employees
=> []
:007> e.save
=> true
:008> c = Company.first
=> #<Company id: 1, name: nil>
:009> c.employees
=> [#<Employee id: 1, name: "foo", city: "bar", company_id: 1>]
:010> e = c.employees.find_or_initialize_by_name(:name => 'foo', :city => 'baz')
=> #<Employee id: 1, name: "foo", city: "bar", company_id: 1>
:011> e.city = 'baz'
=> "baz"
:012> c.employees
=> [#<Employee id: 1, name: "foo", city: "bar", company_id: 1>]
:013 > c.save
=> true
:014> c.employees
=> [#<Employee id: 1, name: "foo", city: "bar", company_id: 1>]
Problems
:004 => The Employee from :003 is not added to c.employees
:006 => The Employee from :003 is saved with c
:010 => The city attribute of employee is not set
:014 => THe city attribute of employee is not updated when saving company

How about this?
employee_attrs = {:name => 'foo', :city => 'bar'}
e = c.employees.where(employee_attrs).first || c.employees.build(employee_attrs)

For the record, here is the implementation I came with. It can probably be simpler, but it suits my needs:
module ActiveRecord
module Associations
class AssociationCollection < AssociationProxy
alias_method :old_method_missing, :method_missing
def method_missing(method_id, *arguments, &block)
if /^find_or_build_by_([_a-zA-Z]\w*)$/ =~ method_id.to_s
names = $1.split('_and_')
find_or_build_by(names, *arguments)
else
old_method_missing(method_id, *arguments, &block)
end
end
def find_or_build_by(names, *arguments)
values = arguments[0]
throw InvalidArgument unless values.keys.first.kind_of?(String)
record = Array.new(self).find do |r|
names.inject(true) do |memo, name|
memo && (values[name].to_s == r.send(name).to_s)
end
end
if record
sanitized_values = record.send(:sanitize_for_mass_assignment, values)
sanitized_values.each {|k, v| record.send("#{k}=", v)}
else
record = build(values)
end
return record
end
end
end
end

I tried the following code for my Rails 4.2.x app.
#config/initializers/collection_proxy.rb
ActiveRecord::Associations::CollectionProxy.class_eval do
alias_method :old_method_missing, :method_missing
def method_missing(method_id, *arguments, &block)
if /^find_or_build_by([_a-zA-Z]\w*)$/ =~ method_id.to_s
names = $1.split('_and_')
find_or_build_by(names, *arguments)
else
old_method_missing(method_id, *arguments, &block)
end
end
def find_or_build_by(names, *arguments)
where(names).first || build(names)
end
end
You can use it like this.
XXX.find_or_build_by(attributes)

Related

How to merge two ActiveRecord queries into one hash and convert to JSON?

I'm using AJAX to get some results, but the problem is I'm querying two separate models and I want to return both and their relationship to one another as one JSON object.
Here's an example of two models I'm trying to link together -
Car
belongs_to :user
:id
:make
:year
:user_id
User
has_many :cars
:id
:first_name
:last_name
:birthday
I'm trying to get it to look something like this -
{
1: {
id: 1,
first_name: 'Joe'
last_name: 'Smith'
cars: {
23: {
id: 23,
make: 'BMW',
year: 2009,
user_id: 1
},
24: {
id: 24,
make: 'Volvo',
year: 2012,
user_id: 1
}
}
},
2: {
id: 2,
first_name: 'Bob'
last_name: 'Johnson'
cars: {
35: {
id: 35,
make: 'Ford',
year: 2013,
user_id: 2
}
}
}
}
Create a new (private) method in your controller:
def format_json(users)
result = {}
users.each do |user|
result[user.id] = user.formatted_data
end
return result
end
Change the action to return:
users = Users.includes(:cars).where("<your_where_clause>").limit(<n>)
render :json => { :result => format_json(users) }.to_json
app/models/user.rb
class User < ActiveRecord::Base
def formatted_data
{
:id => self.id,
:first_name => self.first_name,
:last_name => self.last_name,
:cars => self.get_car_info
}
end
def get_car_info
car_info = {}
self.cars.each do |car|
car_info[car.id] = car.info
end
return car_info
end
end
app/models/car.rb
class Car < ActiveRecord::Base
def info
{
:id => self.id,
:make => self.make,
:year => self.year,
:user_id => self.user_id
}
end
end
I ended up using the .as_json I found detailed here to convert ActiveRecord to a plain Ruby hash and passed that hash to the view.
user = User.find(params[:user_id].to_i)
#json = {}
#json[user.id] = user.as_json
#json[user.id]['cars'] = {}
user.cars.each do |car|
#json[user.id]['cars'][car.id] = car.as_json
end
render json: { status: true, users: #json }

Rails delete table row via migration

I'm trying to delete several rows in the table actionable_items via the following migration. I have debugged and can confirm that the variables that store the table row are not nil. The migration runs successfully, but it doesn't delete the row from the table. Also, does anyone know why I can debug a migration when I run rake db:migrate:redo but not when I run rake db:migrate ?
class RemoveActionableItems < ActiveRecord::Migration
class ActionableItem < ActiveRecord::Base
attr_accessible :actionable_item, :name, :sequence, :type
end
class MenuItemTEMP < ActionableItem
self.table_name = "actionable_items"
end
class InsightReportMenuItemTEMP < ActionableItem
self.table_name = "actionable_items"
end
def up
validation_settings = MenuItem.find_by_name("Validation Settings")
identifier_lookup = MenuItem.find_by_name("Identifier Lookup")
compliance = InsightReportMenuItem.find_by_name("Compliance")
debugger
validation_settings.destroy! #unless validation_settings.nil?
identifier_lookup.destroy! #unless identifier_lookup.nil?
compliance.destroy! #unless compliance.nil?
end
def down
MenuItem.create :name => "Validation Settings", :type => "MenuItem"
MenuItem.create :name => "Identifier Lookup", :type => "MenuItem"
InsightReportMenuItem.create :name => "Compliance", :type => "InsightReportMenuItem"
end
end
I also tried deleting from the rails console, but once again, pgAdmin is showing the row not deleted.
pmpaware-webapp(development)> compliance = InsightReportMenuItem.find_by_name("Compliance")
InsightReportMenuItem Load (3.8ms) SELECT "actionable_items".* FROM "actionable_items" WHERE "actionable_items"."type" IN ('InsightReportMenuItem') AND "actionable_items"."name" = 'Compliance' LIMIT 1
=> #<InsightReportMenuItem id: 264, name: "Compliance", actionable_item_id: nil, created_at: "2015-07-23 18:57:25", updated_at: "2015-07-23 18:57:25", actionable_items_count: 0, sequence: nil, type: "InsightReportMenuItem">
pmpaware-webapp(development)> compliance.errors
=> #<ActiveModel::Errors:0x007fc0735ac540 #base=#<InsightReportMenuItem id: 264, name: "Compliance", actionable_item_id: nil, created_at: "2015-07-23 18:57:25", updated_at: "2015-07-23 18:57:25", actionable_items_count: 0, sequence: nil, type: "InsightReportMenuItem">, #messages={}>
pmpaware-webapp(development)> compliance.delete
SQL (111829.8ms) DELETE FROM "actionable_items" WHERE "actionable_items"."type" IN ('InsightReportMenuItem') AND "actionable_items"."id" = 264
=> #<InsightReportMenuItem id: 264, name: "Compliance", actionable_item_id: nil, created_at: "2015-07-23 18:57:25", updated_at: "2015-07-23 18:57:25", actionable_items_count: 0, sequence: nil, type: "InsightReportMenuItem">
FOUND SOLUTION
class RemoveActionableItems < ActiveRecord::Migration
class ActionableItem < ActiveRecord::Base
attr_accessible :actionable_item, :name, :sequence, :type
end
class MenuItemTEMP < ActionableItem
self.table_name = "actionable_items"
end
class InsightReportMenuItemTEMP < ActionableItem
self.table_name = "actionable_items"
end
def up
MenuItem.delete_all(name: "Validation Settings") unless MenuItem.find_by_name("Validation Settings").nil?
MenuItem.delete_all(name: "Identifier Lookup") unless MenuItem.find_by_name("Identifier Lookup").nil?
InsightReportMenuItem.delete_all(name: "Compliance") unless InsightReportMenuItem.find_by_name("Compliance").nil?
end
def down
MenuItem.create :name => "Validation Settings", :type => "MenuItem"
MenuItem.create :name => "Identifier Lookup", :type => "MenuItem"
InsightReportMenuItem.create :name => "Compliance", :type => "InsightReportMenuItem"
end
end

How to create associated objects (you have accepted parameters) for after saving in Rails?

The problem I am having with this is Product is trying to create variants before the product is even created and there are certain callbacks for variants that require the product to exist. So how can I rewrite this so that v.save doesn't execute till the object is created or whatever.
Product.class_eval do
validates_presence_of [:style_no, :market_price, :designer, :short_description, :description]
validates_numericality_of [:size_47_quantity,
:size_46_quantity,
:size_45_quantity,
:size_44_quantity,
:size_43_quantity,
:size_42_quantity,
:size_41_quantity,
:size_40_quantity,
:size_39_quantity]
for i in 39..47
define_method:"size_#{i}_quantity" do
if v = self.variants.find_by_size(i)
v.count_on_hand
else
0
end
end
define_method:"size_#{i}_quantity=" do |amount|
# if only there is some method that can postpone all the following if this product hasn't been created yet!
self.id = Product.last.id + 1 unless self.id
v = self.variants.find_by_size(i) || self.variants.new(:size => i)
v.count_on_hand = amount
v.save
end
end
end
You can try this solution:
Product class
class Product < ActiveRecord::Base
validates_presence_of [:style_no, :market_price, :designer, :short_description, :description]
has_many :variants
# This method would check if variant was created or loaded.
#
# So many sequantial calls to it will return same object
def variant_with_size(size)
self.variants.select{|v| v.size == size}.first || self.variants.where('size = ?', size).first
end
module ClassExtensions
def self.included(base)
(39..47).each do |i|
method = "size_#{i}_quantity".to_sym
included_module = Module.new
included_module.module_eval <<EOF
def #{method}
if v = self.variant_with_size(#{i})
v.count_on_hand
else
0
end
end
def #{method}=(amount)
v = self.variant_with_size(#{i}) || self.variants.build(:size => #{i})
v.count_on_hand = amount
v
end
EOF
base.send :include, included_module
end
end
end
include ClassExtensions
end
Variant class
class Variant < ActiveRecord::Base
belongs_to :product
validates :count_on_hand, :numericality => true
end
Usage
Usage example with correct variant amount:
ruby-1.9.2-p180 :001 > p = Product.new
=> #<Product id: nil, style_no: nil, market_price: nil, designer: nil, short_description: nil, description: nil, created_at: nil, updated_at: nil>
ruby-1.9.2-p180 :002 > p.size_39_quantity
=> 0
ruby-1.9.2-p180 :003 > p.size_39_quantity = 2
=> 2
ruby-1.9.2-p180 :004 > p.variants
=> [#<Variant id: nil, product_id: nil, size: 39, count_on_hand: 2, created_at: nil, updated_at: nil>]
ruby-1.9.2-p180 :005 > p.save
=> true
ruby-1.9.2-p180 :006 > p.variants
=> [#<Variant id: 3, product_id: 3, size: 39, count_on_hand: 2, created_at: "2011-04-06 06:34:46", updated_at: "2011-04-06 06:34:46">]
Usage with incorrect variant amount:
ruby-1.9.2-p180 :007 > p1 = Product.new
=> #<Product id: nil, style_no: nil, market_price: nil, designer: nil, short_description: nil, description: nil, created_at: nil, updated_at: nil>
ruby-1.9.2-p180 :008 > p1.size_39_quantity = 'A'
=> "A"
ruby-1.9.2-p180 :009 > p1.save
=> false
ruby-1.9.2-p180 :010 > p1.errors
=> {:variants=>["is invalid"]}
ruby-1.9.2-p180 :011 > p1.variants[0].errors
=> {:count_on_hand=>["is not a number"]}
At a glance, I'd consider using an after_save callback on Product to create product variants.
Something like:
class Product < ActiveRecord::Base
has_many :variants
after_save :create_variants! if :not_a_variant?
OPTIONS = [:size_1_qty, :size_2_qty] # TODO: move to a OptionType model associated with Product
def not_a_variant?
size.nil? # or however you might distinguish a Product from a Variant
end
private
def create_variants!
# OPTIONS could instead be related option_types. perhaps a 'size' option type with values of 40, 41, 42, etc.
OPTIONS.each do |size|
variants.build(...)
end
save!
end
end
I was just reviewing the Spree shopping cart project by Rails Dog and they handle product variants in a similar fashion. You might check it out.

Sending objects values as IN statement in Rails

I'm working on a Newsfeed action. I have active friendships and i would like to find the posts only of those friendships to create the newsfeed.
This is the code i currently have:
#active_friendships = current_user.active_friendships
#posts = Post.where({ :user_id => [5,8,16] }).order("created_at DESC")
I don't know how to send the #active_friendships.user_id values to the IN in the second line. At the moment the code is only worked because it has hardcoded the user_id of my active friends (5,8,16).
(If i do a debug #active_friendships) i get the 3 objects with their ids, but i still don't know how to send them to the IN in the second line as the ids to look for.
--
- !ruby/object:Friendship
attributes:
created_at: 2010-10-06 22:27:54.620007
updated_at: 2010-10-07 00:19:10.329799
id: 182
user_id: 8
status: 1
friend_id: 5
attributes_cache: {}
changed_attributes: {}
destroyed: false
marked_for_destruction: false
new_record: false
previously_changed: {}
readonly: false
- !ruby/object:Friendship
attributes:
created_at: 2010-10-07 19:13:10.617959
updated_at: 2010-10-07 19:13:17.097514
id: 192
user_id: 16
status: 1
friend_id: 5
attributes_cache: {}
changed_attributes: {}
destroyed: false
marked_for_destruction: false
new_record: false
previously_changed: {}
readonly: false
- !ruby/object:Friendship
attributes:
created_at: 2010-10-10 04:12:48.931120
updated_at: 2010-10-10 04:12:56.960752
id: 214
user_id: 8
status: 1
friend_id: 5
attributes_cache: {}
changed_attributes: {}
destroyed: false
marked_for_destruction: false
new_record: false
previously_changed: {}
readonly: false
Many thanks in advance
You can try this:
#posts = Post.where(:user_id => #active_friendships.map(&:friend_id)
).order("created_at DESC"
But better way of implementing this is to add an association
class User
has_many :active_friendships, :class_name => "Friendship", :conditions = {...}
has_many :active_friend_posts, :through => :active_friendships,
:source => :friend_posts
end
class Friendship
belongs_to :user
belongs_to :friend, :class_name => "User"
has_many :friend_posts, :class_name => "Post",
:primary_key => :freind_id, :foreign_key => :post_id
end
class Post
belongs_to :user
end
Now you can do the following:
current_user.active_friend_posts.order("created_at DESC")
I'm new at this, but could you collect all of the Friendship.user_id's into an array and then pass that in the where clause?
#friends_userids = current_user.active_friendships.collect { |x| x.user_id }
#posts = Post.where({ :user_id => #friends_userids }).order("created_at DESC")
I had a similar issue. This is how I implemented it:
ids = #active_friendships.map { |x| x.user_id }
ids = ids.join(",")
#posts = Post.all(:conditions => ["posts.user_id in (#{ids})"]).order("created_at DESC")
This creates an array of all user IDs from #active_friendships and then joins them into a string with a comma as a separator. Then I simply passed this to the :conditions parameter.
It's not as elegant as I like but it works.
Hope this helps.
Rails has a special method for getting an array of associated ids. You should be able to do this in one line something like this:
#posts = Post.where(:user_id => current_user.active_friendship_ids).order("created_at DESC")
Whenever you're dealing with has_many and has_many :through relationships, you have access to the association_ids method, which won't return the full objects. Check out the full api here:
http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html

Retrieve all association's attributes of an AR model?

What do you think is the most optimal way to retrieve all attributes for all the associations an AR model has?
i.e: let's say we have the model Target.
class Target < ActiveRecord::Base
has_many :countries
has_many :cities
has_many :towns
has_many :colleges
has_many :tags
accepts_nested_attributes_for :countries, :cities, ...
end
I'd like to retrieve all the association's attributes by calling a method on a Target instance:
target.associations_attributes
>> { :countries => { "1" => { :name => "United States", :code => "US", :id => 1 },
"2" => { :name => "Canada", :code => "CA", :id => 2 } },
:cities => { "1" => { :name => "New York", :region_id => 1, :id => 1 } },
:regions => { ... },
:colleges => { ... }, ....
}
Currently I make this work by iterating on each association, and then on each model of the association, But it's kind of expensive, How do you think I can optimize this?
Just a note: I realized you can't call target.countries_attributes on has_many associations with nested_attributes, one_to_one associations allow to call target.country_attributes
I'm not clear on what you mean with iterating on all associations. Are you already using reflections?
Still curious if there's a neater way, but this is what I could come up with, which more or less results in the hash you're showing in your example:
class Target < ActiveRecord::Base
has_many :tags
def associations_attributes
# Get a list of symbols of the association names in this class
association_names = self.class.reflect_on_all_associations.collect { |r| r.name }
# Fetch myself again, but include all associations
me = self.class.find self.id, :include => association_names
# Collect an array of pairs, which we can use to build the hash we want
pairs = association_names.collect do |association_name|
# Get the association object(s)
object_or_array = me.send(association_name)
# Build the single pair for this association
if object_or_array.is_a? Array
# If this is a has_many or the like, use the same array-of-pairs trick
# to build a hash of "id => attributes"
association_pairs = object_or_array.collect { |o| [o.id, o.attributes] }
[association_name, Hash[*association_pairs.flatten(1)]]
else
# has_one, belongs_to, etc.
[association_name, object_or_array.attributes]
end
end
# Build the final hash
Hash[*pairs.flatten(1)]
end
end
And here's an irb session through script/console to show how it works. First, some environment:
>> t = Target.create! :name => 'foobar'
=> #<Target id: 1, name: "foobar">
>> t.tags.create! :name => 'blueish'
=> #<Tag id: 1, name: "blueish", target_id: 1>
>> t.tags.create! :name => 'friendly'
=> #<Tag id: 2, name: "friendly", target_id: 1>
>> t.tags
=> [#<Tag id: 1, name: "blueish", target_id: 1>, #<Tag id: 2, name: "friendly", target_id: 1>]
And here's the output from the new method:
>> t.associations_attributes
=> {:tags=>{1=>{"id"=>1, "name"=>"blueish", "target_id"=>1}, 2=>{"id"=>2, "name"=>"friendly", "target_id"=>1}}}
try this with exception handling:
class Target < ActiveRecord::Base
def associations_attributes
tmp = {}
self.class.reflections.symbolize_keys.keys.each do |key|
begin
data = self.send(key) || {}
if data.is_a?(ActiveRecord::Base)
tmp[key] = data.attributes.symbolize_keys!
else
mapped_data = data.map { |item| item.attributes.symbolize_keys! }
tmp[key] = mapped_data.each_with_index.to_h.invert
end
rescue Exception => e
tmp[key] = e.message
end
end
tmp
end
end
This is updated version of Stéphan Kochen's code for Rails 4.2
def associations_attributes
association_names = self.class.reflect_on_all_associations.collect { |r| r.name }
me = self.class.includes(association_names).find self.id
pairs = association_names.collect do |association_name|
object_or_array = me.send(association_name)
if object_or_array.is_a? ActiveRecord::Associations::CollectionProxy
association_pairs = object_or_array.collect { |o| [o.id, o.attributes] }
[association_name, Hash[*association_pairs.flatten(1)]]
else
[association_name, object_or_array.attributes]
end
end
Hash[*pairs.flatten(1)]
end

Resources