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.
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
I basically want to create a concern which will be included in all the polymorphic models. This concern needs to have a dynamic setter method which which sets the value for the '_type' column.
module StiPolymorphable
extend ActiveSupport::Concern
included do
define_method "#{magic_method_to_get_type_column}=" do |type_field|
super(type_field.to_s.classify.constantize.base_class.to_s)
end
end
end
I basically want to access all the addresses of a Parent instance instead of a Person instance.
Example -
Suppose I have the following classes
class Person < ActiveRecord::Base
end
class Parent < Person end
class Teacher < Person end
class Address < ActiveRecord::Base
include StiPolymorphable
belongs_to :addressable, polymorphic: true
end
Right now if I try to access the addresses of a Parent it gives me zero records since the addressable_type field contains the value 'Person'.
Parent.first.addresses => #<ActiveRecord::Associations::CollectionProxy []>
Person.first.addresses => #<ActiveRecord::Associations::CollectionProxy [#<Address id: .....>]>
You might be interested on looking at Modularity gem so you could pass variables when you're including the Module. Haven't really tried it though. Hope it helps.
We do something like this:
module Shared::PolymorphicAnnotator
extend ActiveSupport::Concern
class_methods do
# #return [String]
# the polymorphic _id column
def annotator_id
reflections[annotator_reflection].foreign_key.to_s
end
# #return [String]
# the polymorphic _type column
def annotator_type
reflections[annotator_reflection].foreign_type
end
end
included do
# Concern implementation macro
def self.polymorphic_annotates(polymorphic_belongs, foreign_key = nil)
belongs_to polymorphic_belongs.to_sym, polymorphic: true, foreign_key: (foreign_key.nil? ? (polymorphic_belongs.to_s + '_id').to_s : polymorphic_belongs.to_s)
alias_attribute :annotated_object, polymorphic_belongs.to_sym
define_singleton_method(:annotator_reflection){polymorphic_belongs.to_s}
end
attr_accessor :annotated_global_entity
# #return [String]
# the global_id of the annotated object
def annotated_global_entity
annotated_object.to_global_id if annotated_object.present?
end
# #return [True]
# set the object when passed a global_id String
def annotated_global_entity=(entity)
o = GlobalID::Locator.locate entity
write_attribute(self.class.annotator_id, o.id)
write_attribute(self.class.annotator_type, o.class.base_class)
true
end
end
end
In your model:
class Foo
include Shared::PolymorphicAnnotator
polymorphic_annotates('belongs_to_name', 'foreign_key')
end
I have a local user model which uses activerecord. The user has an email field. I also have an activeresource model called tasks which has a created_by field which stores the submitting users email address. I'd like to link the two but I'm struggling with the right syntax or even whether it's possible.
The main branch of ActiveResource doesn't even seem to support foreign key. I found an alternative branch but still couldn't get anything working.
class User < ActiveRecord::Base
has_many :tasks
end
class Task < ActiveResource::Base
belongs_to :user
schema do
string 'created_by' #email
# other fields
end
end
Your code should work fine given that you have a user_id attribute on the Task to act as the foreign key, or you can specify the foreign key in the association on User model as follows:
class User < ActiveRecord::Base
has_many :tasks, foreign_key: "uid"
end
Now the problem is, you can't use belongs_to on ActiveResource so unless you need to retrieve the user from an instance of the Task class, you can just remove it and the other side of the relation will still work, however, if you need to retrieve the user then you'd have to either implement your own finder method as follows:
class Task < ActiveResource::Base
schema do
string 'created_by' #email
# other fields
end
def user
#user ||= User.find self.user_id # use the attribute name that represents the foreign key
end
def user=(user)
#user = user
self.update_attribute(:user_id, user.id)
end
end
This will basically behave the same as you would expect on an ActiveRecord model, however this can be tiresome if you have multiple associations so you can instead extend the ActiveResource module to add belongs_to as follows:
module BelongsToActiveResource
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def belongs_to( name, options = {} )
class_eval %(
def #{name}
##{name} ||= name.to_s.classify.find( name.id )
end
def #{name}=(obj)
##{name} ||= obj
self.update_attribute((name.to_s + "_id").to_sym, ##{name}.id
end
)
end
end
end
ActiveResource::Base.class_eval { include BelongsToActiveResource }
This will allow you to use belongs_to on any ActiveResource model.
P.S.: the above solution was inspired by https://stackoverflow.com/a/8844932/3770684
You can't, but you can fake it by implementing accessor methods yourself.
class User < ActiveRecord::Base
#has_many :tasks
def tasks
Task.find(:all, params: {created_by: email})
end
end
class Task < ActiveResource::Base
#belongs_to :user
def user
User.where(email: created_by).first
end
schema do
string 'created_by' #email
# other fields
end
end
This will let you write code as if your objects were associated (i.e. User.first.tasks.length). However, they aren't actually joined. That means calling User.first.tasks is going to hit the database, then make an additional HTTP request to retrieve the Tasks. Depending on how your code is structured, you could run into unexpected performance issues.
Also, you can't run a single query to get a User and all associated Tasks (since it's two separate data stores), and you can't do fancy stuff like User.joins(:tasks).where({tasks: field_1: true}).
I have the following hierarchy of models where each one has_many of the one below it:
class AccountGroup < ActiveRecord::Base
has_many :accounts, :inverse_of=>:account_group
# name: string
class Account < ActiveRecord::Base
belongs_to :accountGroup, :inverse_of=>:account
has_many :positions, :inverse_of=>:account
class Position < ActiveRecord::Base
belongs_to :account, :inverse_of=>:positions
# net_position: integer
In other words, an AccountGroup contains a bunch of Accounts, and an Account contains a bunch of Positions.
Goal: I want an hash of AccountGroup => (sum of its net_positions). That means there's a GROUP BY involved.
I can do this with raw SQL, but I haven't cracked it with Rails functions. The raw SQL is:
SELECT account_groups.id,SUM(net_position),account_groups.name
FROM account_groups
LEFT JOIN accounts ON accounts.account_group_id = account_groups.id
LEFT JOIN positions ON positions.account_id = accounts.id
GROUP BY account_groups.id,account_groups.name;
Is this something that Rails just can't do?
Rails (4.0.0) can do this - we have two ways to do it currently:
1. SQL "Alias" Columns
Rails Scoping For has_many :through To Access Extra Data
#Images
has_many :image_messages, :class_name => 'ImageMessage'
has_many :images, -> { select("#{Image.table_name}.*, #{ImageMessage.table_name}.caption AS caption") }, :class_name => 'Image', :through => :image_messages, dependent: :destroy
2. ActiveRecord Association Extensions
This is a little-known feature of Rails, which allows you to play with the collection object. The way it does it is to extend the has_many relationship you have created:
class AccountGroup < ActiveRecord::Base
has_many :accounts do
def X
#your code here
end
end
end
We have only got this method working for collections, but you can do all sorts with it. You should look at this tutorial to see more about it
Update
We just got this working by using an extension module:
#app/models/message.rb
Class Message < ActiveRecord::Base
has_many :image_messages #-> join model
has_many :images, through: :image_messages, extend: ImageCaption
end
#app/models/concerns/image_caption.rb
module ImageCaption
#Load
def load
captions.each do |caption|
proxy_association.target << caption
end
end
#Private
private
#Captions
def captions
return_array = []
through_collection.each_with_index do |through,i|
associate = through.send(reflection_name)
associate.assign_attributes({caption: items[i]})
return_array.concat Array.new(1).fill( associate )
end
return 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(&:caption)
end
#Target
def target_collection
#load_target
proxy_association.target
end
end
Props to this gist for the variable functions
This basically overrides the load ActiveRecord function in the CollectionProxy class, and uses it to create our own proxy_association.target array :)
If you need any information on how to implement, just ask in the comments
You can make this little bit more prettier than raw sql by using rails AR querying methods:
AccountGroup.
select("account_groups.id, SUM(net_position), account_groups.name").
joins("LEFT JOIN accounts ON accounts.account_group_id = account_groups.id").
joins("LEFT JOIN positions ON positions.account_id = accounts.id").
group("account_groups.id,account_groups.name")
This can be done with pure Arel as well.
AccountGroup.select(
AccountGroup.arel_table[:id], Arel::Nodes::NamedFunction.new('SUM', [:net_position]), AccountGroup.arel_table[:name]
).joins(
AccountGroup.arel_table.join(Account.arel_table).on(
Account.arel_table[:account_group_id].eq(AccountGroup.arel_table[:id])
).join_sources
).joins(
AccountGroup.arel_table.join(Position.arel_table).on(
Position.arel_table[:account_id].eq(Account.arel_table[:id])
).join_sources
).group(
AccountGroup.arel_table[:id], AccountGroup.arel_table[:name]
)
I'm not 100% sure this will work, I simply copied your SQL from above and put it into scuttle.io
Use include function, in example
ac = AccountGroup.all(:include => :account)
$ AccountGroup Load (0.6ms) SELECT `account_groups`.* FROM `groups`
$ Account Load (16.4ms) SELECT `accounts`.* FROM `accounts` WHERE `accounts`.`id` IN (1010, 3, 4, 202, 203, 204, 9999)
Then you can call ac.account.name or something like that
There are a great Railscast http://railscasts.com/episodes/22-eager-loading?view=asciicast
If you really want to use ActiveRecord for this (no SQL), it will be something like:
ags = AccountGroup.all(:include => {:accounts => :positions})
hash = Hash[ags.map { |ag| [ag, ag.map(&:accounts).flatten.map(&:positions).flatten.map(&:net_position).reduce(0,&:+)]}]
But it will be slower than your SQL, and not any prettier.
Is this something that Rails just can't do?
As this question has been open for about a month, I'm gonna to go ahead and assume the answer to this question is...
Yes.
EDIT: Yes, for Rails 3. But Rails 4 can do it! See accepted answer.
Rails can't do it, outside of using find_by_sql or ActiveRecord::Base.connection.execute(query), which are pretty kludgy and not rails-y.
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.