How do I create an array of associations from a given string? - ruby-on-rails

Given I have an AR object (object) and a string (s). s is based out of the associations of object. ie. Product belongs to Aisle, Aisle belongs to Department, Department belongs to Shop
I want to get an array of all those associations. This is what Ive come up with
object = Product.last
s='aisle.department.shop'
new_array = s.split(',').each_with_object([]).with_index do |(assoc_set, array), index|
a = assoc_set.split('.')
a.each_with_index do |assoc, index|
if index == 0
array << object.send(assoc)
elsif index == 1
array << object.send(a[index - 1]).send(a[index])
elsif index == 2
array << object.send(a[index - 2]).send(a[index - 1]).send(a[index])
elsif index == 3
array << object.send(a[index - 3]).send(a[index - 2]).send(a[index - 1]).send(a[index])
end
end
end
Outputs exactly what I want:
[
#<Aisle:0x00007fa001d6f800 ...>,
#<Department:0x00007fa001d44b00 ...>,
#<Shop:0x00007fa020f4bc68 ...>,
]
Except code isnt dynamic. As you can see, it only goes up to 3 levels deep. How can I refactor this?

This should work as you wanted
object = Product.last
relations_chain='aisle.department.shop'
relations_chain.split('.').each_with_object([]).with_index do |(relation_name, array), index|
prev_object = index.zero? ? object : array[index - 1]
array << prev_object.send(relation_name)
end
You can delegate relations and call everything on Product model.
class Department < ApplicationRecord
delegate :shop, to: :department, allow_nil: true
end
class Product < ApplicationRecord
delegate :department, to: :aisle, allow_nil: true
delegate :shop, to: :aisle, allow_nil: true
end
relations_chain.split('.').map { |relation_name| object.send(relation_name) }

Related

How To Fix undefined method `product' for #<LineItem::ActiveRecord_Relation:0x0000000017b22f70>

I'm try product quantity - 1 but ı get this error
line_item.rb
belongs_to :order
belongs_to :product
payment.rb
has_many :orders
undefined method `product' for # LineItem::ActiveRecord_Relation:0x0000000017b22f70>
#line_item = LineItem.where(:order_id => params[:zc_orderid])
#line_item.product.quantity = #line_item.product.quantity - 1
if #line_item.product.quantity == 0
#line_item.product.sold = true
end
#line_item.product.save
If you use where, you don't get a single LineItem object, but a LineItem::ActiveRecord_Relation object. If that condition is enough to get just one record then use find_by. If it's not you need to think more about the logic because you'd get more than one object.
#line_item = LineItem.find_by(:order_id => params[:zc_orderid])
If you want to decrease the quantity of all those line items I'd do something like
LineItem.transaction do
LineItem.where(:order_id => params[:zc_orderid]).each do |line_item|
line_item.product.quantity = line_item.product.quantity - 1
if line_item.product.quantity == 0
line_item.product.sold = true
end
line_item.product.save
end
end
Since Order has many LineItem you should expect more than one line, so should rewrite your code:
LineItem.where(:order_id => params[:zc_orderid]).each do |line_item|
product = line_item.product
product.quantity -= 1
if product.quantity == 0
product.sold = true
end
product.save
end
Btw, consider add a Transaction.
LineItem.where(:order_id => params[:zc_orderid]) its return as array format.
So you can fetch by following
LineItem.find_by(order_id: params[:zc_orderid]). its return single active record

Rails association scope by method with aggregate

I'm trying to retrieve association records that are dependent on their association records' attributes. Below are the (abridged) models.
class Holding
belongs_to :user
has_many :transactions
def amount
transactions.reduce(0) { |m, t| t.buy? ? m + t.amount : m - t.amount }
end
class << self
def without_empty
includes(:transactions).select { |h| h.amount.positive? }
end
end
class Transaction
belongs_to :holding
attributes :action, :amount
def buy?
action == ACTION_BUY
end
end
The problem is my without_empty method returns an array, which prevents me from using my pagination.
Is there a way to rewrite Holding#amount and Holding#without_empty to function more efficiently with ActiveRecord/SQL?
Here's what I ended up using:
def amount
transactions.sum("CASE WHEN action = '#{Transaction::ACTION_BUY}' THEN amount ELSE (amount * -1) END")END")
end
def without_empty
joins(:transactions).group(:id).having("SUM(CASE WHEN transactions.action = '#{Transaction::ACTION_BUY}' THEN transactions.amount ELSE (transactions.amount * -1) END) > 0")
end

combine keys in array of hashes

I map results of my query to create an array of hashes grouped by organisation_id like so:
results.map do |i|
{
i['organisation_id'] => {
name: capability.name,
tags: capability.tag_list,
organisation_id: i['organisation_id'],
scores: {i['location_id'] => i['score']}
}
}
a capability is defined outside the map.
The result looks like:
[{1=>{:name=>"cap1", :tags=>["tag A"], :scores=>{26=>4}}}, {1=>{:name=>"cap1", :tags=>["tag A"], :scores=>{12=>5}}}, {2 => {...}}...]
For every organisation_id there is a separate entry in the array. I would like to merge these hashes and combine the scores key as so:
[{1=>{:name=>"cap1", :tags=>["tag A"], :scores=>{26=>4, 12=>5}}}, {2=>{...}}... ]
EDIT
To create the results I use the following AR:
Valuation.joins(:membership)
.where(capability: capability)
.select("valuations.id, valuations.score, valuations.capability_id, valuations.membership_id, memberships.location_id, memberships.organisation_id")
.map(&:serializable_hash)
A Valuation model:
class Valuation < ApplicationRecord
belongs_to :membership
belongs_to :capability
end
A Membership model:
class Membership < ApplicationRecord
belongs_to :organisation
belongs_to :location
has_many :valuations
end
results snippet:
[{"id"=>1, "score"=>4, "capability_id"=>1, "membership_id"=>1, "location_id"=>26, "organisation_id"=>1}, {"id"=>16, "score"=>3, "capability_id"=>1, "membership_id"=>2, "location_id"=>36, "organisation_id"=>1}, {"id"=>31, "score"=>3, "capability_id"=>1, "membership_id"=>3, "location_id"=>26, "organisation_id"=>2}, {"id"=>46, "score"=>6, "capability_id"=>1, "membership_id"=>4, "location_id"=>16, "organisation_id"=>2}...
I'll assume for each organization: the name, taglist and organization_id remains the same.
your_hash = results.reduce({}) do |h, i|
org_id = i['organisation_id']
h[org_id] ||= {
name: capability.name,
tags: capability.taglist,
organisation_id: org_id,
scores: {}
}
h[org_id][:scores][i['location_id']] = i['score']
# If the location scores are not strictly exclusive, you can also just +=
h
end
I believe this works, but data is needed to test it.
results.each_with_object({}) do |i,h|
h.update(i['organisation_id'] => {
name: capability.name,
tags: capability.tag_list,
organisation_id: i['organisation_id'],
scores: {i['location_id'] => i['score']}) { |_,o,n|
o[:scores].update(n[:score]); o }
}
end.values
This uses the form of Hash#update (aka merge!) that uses a block to determine the values of keys that are present in both hashes being merged. Please consult the doc for the contents of each of the block variables _, o and n.
Assume, that result is your final array of hashes:
result.each_with_object({}) do |e, obj|
k, v = e.flatten
if obj[k]
obj[k][:scores] = obj[k][:scores].merge(v[:scores])
else
obj[k] = v
end
end

Instance Variables in a Rails Model

I have this variable opinions I want to store as an instance variable in my model... am I right in assuming I will need to add a column for it or else be re-calculating it constantly?
My other question is what is the syntax to store into a column variable instead of just a local one?
Thanks for the help, code below:
# == Schema Information
#
# Table name: simulations
#
# id :integer not null, primary key
# x_size :integer
# y_size :integer
# verdict :string
# arrangement :string
# user_id :integer
#
class Simulation < ActiveRecord::Base
belongs_to :user
serialize :arrangement, Array
validates :user_id, presence: true
validates :x_size, :y_size, presence: true, :numericality => {:only_integer => true}
validates_numericality_of :x_size, :y_size, :greater_than => 0
def self.keys
[:soft, :hard, :none]
end
def generate_arrangement
#opinions = Hash[ Simulation.keys.map { |key| [key, 0] } ]
#arrangement = Array.new(y_size) { Array.new(x_size) }
#arrangement.each_with_index do |row, y_index|
row.each_with_index do |current, x_index|
rand_opinion = Simulation.keys[rand(0..2)]
#arrangement[y_index][x_index] = rand_opinion
#opinions[rand_opinion] += 1
end
end
end
def verdict
if #opinions[:hard] > #opinions[:soft]
:hard
elsif #opinions[:soft] > #opinions[:hard]
:soft
else
:push
end
end
def state
#arrangement
end
def next
new_arrangement = Array.new(#arrangement.size) { |array| array = Array.new(#arrangement.first.size) }
#opinions = Hash[ Simulation.keys.map { |key| [key, 0] } ]
#seating_arrangement.each_with_index do |array, y_index|
array.each_with_index do |opinion, x_index|
new_arrangement[y_index][x_index] = update_opinion_for x_index, y_index
#opinions[new_arrangement[y_index][x_index]] += 1
end
end
#arrangement = new_arrangement
end
private
def in_array_range?(x, y)
((x >= 0) and (y >= 0) and (x < #arrangement[0].size) and (y < #arrangement.size))
end
def update_opinion_for(x, y)
local_opinions = Hash[ Simulation.keys.map { |key| [key, 0] } ]
for y_pos in (y-1)..(y+1)
for x_pos in (x-1)..(x+1)
if in_array_range? x_pos, y_pos and not(x == x_pos and y == y_pos)
local_opinions[#arrangement[y_pos][x_pos]] += 1
end
end
end
opinion = #arrangement[y][x]
opinionated_neighbours_count = local_opinions[:hard] + local_opinions[:soft]
if (opinion != :none) and (opinionated_neighbours_count < 2 or opinionated_neighbours_count > 3)
opinion = :none
elsif opinion == :none and opinionated_neighbours_count == 3
if local_opinions[:hard] > local_opinions[:soft]
opinion = :hard
elsif local_opinions[:soft] > local_opinions[:hard]
opinion = :soft
end
end
opinion
end
end
ActiveRecord analyzes the database tables and creates setter and getter methods with metaprogramming.
So you would create a database column with a migration:
rails g migration AddOpinionToSimulation opinion:hash
Note that not all databases support storing a hash or a similar key/value data type in a column. Postgres does. If you need to use another database such MySQL you should consider using a relation instead (storing the data in another table).
Then when you access simulation.opinion it will automatically get the database column value (if the record is persisted).
Since ActiveRecord creates a setter and getter you can access your property from within the Model as:
class Simulation < ActiveRecord::Base
# ...
def an_example_method
self.opinions # getter method
# since self is the implied receiver you can simply do
opinions
opinions = {foo: "bar"} # setter method.
end
end
The same applies when using the plain ruby attr_accessor, attr_reader and attr_writer macros.
When you assign to an attribute backed by a database column ActiveRecord marks the attribute as dirty and will include it when you save the record.
ActiveRecord has a few methods to directly update attributes: update, update_attributes and update_attribute. There are differences in the call signature and how they handle callbacks.
you can add a method like
def opinions
#opinions ||= Hash[ Simulation.keys.map { |key| [key, 0] }
end
this will cache the operation into the variable #opinions
i would also add a method like
def arrangement
#arrangement ||= Array.new(y_size) { Array.new(x_size) }
end
def rand_opinion
Simulation.keys[rand(0..2)]
end
and then replace the variables with your methods
def generate_arrangement
arrangement.each_with_index do |row, y_index|
row.each_with_index do |current, x_index|
arrangement[y_index][x_index] = rand_opinion
opinions[rand_opinion] += 1
end
end
end
now your opinions and your arrangement will be cached and the code looks better. you didn't have to add a new column in you table
you now hat to replace the #opinions variable with your opinions method

N+1 while enumerating self-referencing records

I'm doing a pretty basic thing - displaying a tree of categories in topological order and ActiveRecord issues extra query for enumerating each category's children.
class Category < ActiveRecord::Base
attr_accessible :name, :parent_id
belongs_to :parent, :class_name => 'Category'
has_many :children, :class_name => 'Category', :foreign_key => 'parent_id'
def self.in_order
all = Category.includes(:parent, :children).all # Three queries as it should be
root = all.find{|c| c.parent_id == nil}
queue = [root]
result = []
while queue.any?
current = queue.shift
result << current
current.children.each do |child| # SELECT * FROM categories WHERE parent_id = ?
queue << child
end
end
result
end
end
UPD. As far as I understand what's going here is that when a category is referred as a children of some category it's not the same object as the one in the initial list and so it hasn't it's children loaded. Is there a way to implement desired behavior without resorting to creating extra adjacency list?
UPD2: Here's the manual adjacency list solution. It uses only one query but I'd really like to use something more idiomatic
def self.in_order_manual
cache = {}
adj = {}
root = nil
all.each do |c|
cache[c.id] = c
if c.parent_id != nil
(adj[c.parent_id] ||= []) << c.id
else
root = c.id
end
end
queue = [root]
result = []
while queue.any?
current = queue.shift
result << current
(adj[current] || []).each{|child| queue << child}
end
result.map{|id| cache[id]}
end

Resources