My application configuration includes some values which need to be used in AR relationships. I'm aware this is an odd and potentially criminal thing to attempt, but I need to maintain the configuration as a textfile, and I honestly think I have a good case for a tableless model. Unfortunately I'm having trouble convincing AR (Rails 3.2) not to look for the table. My tableless model:
class Tableless < ActiveRecord::Base
def self.table_name
self.name.tableize
end
def self.columns
#columns ||= [];
end
def self.column(name, sql_type = nil, default = nil, null = true)
columns << ActiveRecord::ConnectionAdapters::Column.new(name.to_s, default, sql_type.to_s, null)
end
def self.columns_hash
#columns_hash ||= Hash[columns.map { |column| [column.name, column] }]
end
def self.column_names
#column_names ||= columns.map { |column| column.name }
end
def self.column_defaults
#column_defaults ||= columns.map { |column| [column.name, nil] }.inject({}) { |m, e| m[e[0]] = e[1]; m }
end
def self.descends_from_active_record?
return true
end
def persisted?
return false
end
def save( opts = {} )
options = { :validate => true }.merge(opts)
options[:validate] ? valid? : true
end
end
This is extended by the actual model:
class Stuff < Tableless
has_many :stuff_things
has_many :things, :through => :stuff_things
column :id, :integer
column :name, :string
column :value, :string
def initialize(attributes = {})
attributes.each do |name, value|
send("#{name}=", value)
end
end
end
This is all based on code found here on SO and elsewhere, but alas, I get SQLException: no such table: stuffs: Any clues any one?
For Rails >= 3.2 there is the activerecord-tableless gem. Its a gem to create tableless ActiveRecord models, so it has support for validations, associations, types.
When you are using the recommended way (using ActiveModel opposed to ActiveRecord) to do it in Rails 3.x there is no support for association nor types.
For Rails >= 4 you can also get support for validations, associations, and some callbacks (like after_initialize) by defining your tableless classes like this:
class Tableless < ActiveRecord::Base
def self.columns() #columns ||= []; end
def self.column(name, sql_type = nil, default = nil, null = true)
columns << ActiveRecord::ConnectionAdapters::Column.new(name.to_s, default, sql_type.to_s, null)
end
attr_accessor :id, :name, :value
has_many :stuff_things
has_many :things, :through => :stuff_things
end
Related
I have two tables, Member and MemberRecord.
This are their relationship:
# Member Model
class Member < ActiveRecord::Base
has_many :member_records, :dependent => :destroy
end
# MemberRecord Model
class MemberRecord < ActiveRecord::Base
belongs_to :member
end
In MemberRecord There are many columns: two_pointer_attempt, two_pointer_made, three_pointer_attempt, three_pointer_made, free_throws_attempt, free_throws_made, offensive_rebound, defensive_rebound, assist, block, steal, turnover, foul, score
Can I get those columns sum in more efficient way?
This is what I did so far:
class Member < ActiveRecord::Base
belongs_to :team
has_many :member_records, :dependent => :destroy
validates :name, :number, presence: true
validates_uniqueness_of :name, scope: :team_id
validates_inclusion_of :number, in: 0..99
def sum_two_pointer_made
self.member_records.sum(:two_pointer_made)
end
def sum_two_pointer_attempt
self.member_records.sum(:two_pointer_attempt)
end
def sum_two_pointer_total
sum_two_pointer_made + sum_two_pointer_attempt
end
def sum_three_pointer_made
self.member_records.sum(:three_pointer_made)
end
def sum_three_pointer_attempt
self.member_records.sum(:three_pointer_attempt)
end
def sum_three_pointer_total
sum_three_pointer_made + sum_three_pointer_attempt
end
def sum_free_throws_made
self.member_records.sum(:free_throws_made)
end
def sum_free_throws_attempt
self.member_records.sum(:free_throws_attempt)
end
def sum_free_throws_total
sum_free_throws_made + sum_free_throws_attempt
end
def sum_offensive_rebound
self.member_records.sum(:offensive_rebound)
end
def sum_defensive_rebound
self.member_records.sum(:defensive_rebound)
end
def sum_assist
self.member_records.sum(:assist)
end
def sum_block
self.member_records.sum(:block)
end
def sum_steal
self.member_records.sum(:steal)
end
def sum_turnover
self.member_records.sum(:turnover)
end
def sum_foul
self.member_records.sum(:foul)
end
def sum_score
self.member_records.sum(:score)
end
end
I will give you an example with two columns and you can extend it for your number of columns.
class Member < ActiveRecord::Base
# add associations here as already present
MR_SUM_COLUMNS = %w{
assist
block
} # add more member record columns here
MR_SUM_COLUMNS.each do |column|
define_method "member_record_#{column}_sum" do
member_record_sums.send(column)
end
end
private
def member_record_sums
#_member_record_sums ||=
begin
tn = MemberRecord.table_name
sums_str =
MR_SUM_COLUMNS.map do |c|
"SUM(#{tn}.#{c}) AS #{c}"
end.join(', ')
self.member_records.select(sums_str).first
end
end
end
m = Member.first
s1 = m.member_record_assist_sum
s2 = m.member_record_block_sum
Explanation:
In ActiveRecord's select method, you can store the sum of a column as a particular value. For example:
# you have only two members with ids 1 and 2
m = Member.select("SUM(id) AS id_sum").first
m.id_sum #=> 3
So we're storing all sums of member_records in one go: in the member_record_sums method. We are also using an instance variable to store the results so that subsequent calls to the method do not query the database.
From there, all we have to do is define our sum-lookup methods dynamically.
In Rails, what is the inverse of update_attributes! ?
In other words, what maps a record to an attributes hash that would recreate that record and all of it children records?
The answer is not ActiveRecord.attributes as that will not recurse into child object.
To clarify if you have the following:
class Foo < ActiveRecord::Base
has_many :bars
accepts_nested_attributes_for :bars
end
Then you can pass an hash like
{"name" => "a foo", "bars_attributes" => [{"name" => "a bar} ...]}
to update_attributes. But it's not clear how to easily generate such a hash programatically for this purpose.
EDIT:
As I have mentioned in a comment, I can do something like:
foo.as_json(:include => :bars)
but I wanted a solution that uses the accepts_nested_attributes_for :bars declaration to avoid having to explicitly include associations.
Not sure how that would be the "inverse", but while Rails might not "have the answer" per-see, there is nothing stopping you from traversing through an object and creating this VERY efficiently.
Something to get you started:
http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html#method-i-accepts_nested_attributes_for
You'll notice in the accepts_nested_attributes_for method, rails sets a hash for all models nested, in nested_attributes_options. So we can use that to get these nested associations, to populate this new hash.
def to_nested_hash
nested_hash = self.attributes.delete_if {|key, value| [:id, :created_at, :deleted_at].include? key.to_sym } # And any other attributes you don't want
associations = self.nested_attributes_options.keys
associations.each do |association|
key = "#{association}_attributes"
nested_hash[key] = []
self.send(association).find_each do |child|
nested_hash[key] << child.attributes.delete_if {|key, value| [:id, :created_at, :deleted_at].include? key.to_sym }
end
end
return nested_hash
end
OR just thought of this:
Using your example above:
foo.as_json(:include => foo.nested_attributes_options.keys)
One thing to note, this won't give you the bars_attributes where my first suggestions will. (neither will serializable_hash)
You can use the following method to include nested options in hash
class Foo < ActiveRecord::Base
has_many :bars
accepts_nested_attributes_for :bars
def to_nested_hash(options = nil)
options ||= {}
if options[:except]
incl = self.nested_attributes_options.keys.map(&:to_s) - Array(options[:except]).map(&:to_s)
else
incl = self.nested_attributes_options.keys
end
options = { :include => incl }.merge(options)
self.serializable_hash(options)
end
end
If for some situations you don't want bars, you can pass options
foo.to_nested_hash(:except => :bars)
Edit: Another option if you want same behaviour in as_json, to_json and to_xml
class Foo < ActiveRecord::Base
has_many :bars
accepts_nested_attributes_for :bars
def serializable_hash(options = nil)
options ||= {}
if options[:except]
incl = self.nested_attributes_options.keys.map(&:to_s) - Array(options[:except]).map(&:to_s)
else
incl = self.nested_attributes_options.keys
end
options = { :include => incl }.merge(options)
super(options)
end
def to_nested_hash(options = nil)
self.serializable_hash(options)
end
end
After looking at a few tableless solutions in Rails (virtus, active_attr, activemodel) it's clear that Rails associations are not supported. My question is why not? Is there some obvious reason for this that I'm missing? Seems like associations would be extremely useful but in all the examples I've seen they're left out.
I'm not sure how to answer your question of why it is not supported by here is one way you could support it with Rails 4+. This would not require you to have a database table and would also give you access to things like validations, associations, and some callbacks like after_initialize.
class Tableless < ActiveRecord::Base
def self.columns() #columns ||= []; end
def self.column(name, sql_type = nil, default = nil, null = true)
columns << ActiveRecord::ConnectionAdapters::Column.new(name.to_s, default, sql_type.to_s, null)
end
attr_accessor :id, :name, :value
has_many :stuff_things
has_many :things, :through => :stuff_things
end
Let's say I have an Order model, that contains many products. I want to be able to keep track of which products are shipped, and which aren't, so I would like to keep track of some metadata that goes along with each relation. If this was a has_one relation, then it would be simple, just insert a few more fields.
How can I accomplish this with a has_many relation between an Order model and a Product model cleanly using Mongoid?
I solved this myself my using another model OrderProduct as a proxy between the two. The implementation of the state was done using the state_machine gem. I ended up wrapping a lot of the calls to the product's state machine in the Order model. This allows me to call things like .product_state(product) or can_cancel_product?(product) on the order instance.
Order
class Order
include Mongoid::Document
include Mongoid::Timestamps
field :state, type: String
embeds_many :order_products
state_machine initial: :open do
...
end
def products
order_products.map do |op|
op.product
end.freeze
end
def add_product(product)
OrderProduct.create({_product: product._id, order: self})
end
def remove_product(product)
order_products.delete find_order_product(p)
end
#wrapper for product events
OrderProduct.new.state_paths.events.each do |event|
define_method "#{event}_product" do |product|
find_order_product(product).send(event)
end
define_method "can_#{event}_product?" do |product|
find_order_product(product).send("can_#{event}?")
end
end
#wrapper for product states
OrderProduct.state_machine.states.map(&:name).each do |state|
define_method "product_#{state}?" do |product|
find_order_product(product).send("#{state}?")
end
end
#wrapper for product current state
def product_state(product)
find_order_product(product).state
end
private
def find_order_product(p)
order_products.at(order_products.index do |op|
op.product == p
end)
end
end
OrderProduct
class OrderProduct
include Mongoid::Document
embedded_in :order
field :_product, type: Moped::BSON::ObjectId
state_machine initial: :open do
....
end
def product
Product.find(_product)
end
def product=(product)
_product = (product._id)
end
end
I have a rails model that looks something like this:
class Recipe < ActiveRecord::Base
has_many :ingredients
attr_accessor :ingredients_string
attr_accessible :title, :directions, :ingredients, :ingredients_string
before_save :set_ingredients
def ingredients_string
ingredients.join("\n")
end
private
def set_ingredients
self.ingredients.each { |x| x.destroy }
self.ingredients_string ||= false
if self.ingredients_string
self.ingredients_string.split("\n").each do |x|
ingredient = Ingredient.create(:ingredient_string => x)
self.ingredients << ingredient
end
end
end
end
The idea is that when I create the ingredient from the webpage, I pass in the ingredients_string and let the model sort it all out. Of course, if I am editing an ingredient I need to re-create that string. The bug is basically this: how do I inform the view of the ingredient_string (elegantly) and still check to see if the ingredient_string is defined in the set_ingredients method?
Using these two together are probably causing your issues. Both are trying to define an ingredients_string method that do different things
attr_accessor :ingredients_string
def ingredients_string
ingredients.join("\n")
end
Get rid of the attr_accessor, the before_save, set_ingredients method and define your own ingredients_string= method, something like this:
def ingredients_string=(ingredients)
ingredients.each { |x| x.destroy }
ingredients_string ||= false
if ingredients_string
ingredients_string.split("\n").each do |x|
ingredient = Ingredient.create(:ingredient_string => x)
self.ingredients << ingredient
end
end
end
Note I just borrowed your implementation of set_ingredients. There's probably a more elegant way to break up that string and create/delete Ingredient model associations as needed, but it's late and I can't think of it right now. :)
The previous answer is very good but it could do with a few changes.
def ingredients_string=(text)
ingredients.each { |x| x.destroy }
unless text.blank?
text.split("\n").each do |x|
ingredient = Ingredient.find_or_create_by_ingredient_string(:ingredient_string => x)
self.ingredients
I basically just modified Otto's answer:
class Recipe < ActiveRecord::Base
has_many :ingredients
attr_accessible :title, :directions, :ingredients, :ingredients_string
def ingredients_string=(ingredient_string)
ingredient_string ||= false
if ingredient_string
self.ingredients.each { |x| x.destroy }
unless ingredient_string.blank?
ingredient_string.split("\n").each do |x|
ingredient = Ingredient.create(:ingredient_string => x)
self.ingredients << ingredient
end
end
end
end
def ingredients_string
ingredients.join("\n")
end
end