Rails - how to join/group nested tables and get joined values out? - ruby-on-rails

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.

Related

Rails select value into attribute

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.

STI with many types

I have a rails 4 app with STI models:
# models/person.rb
def Person < ActiveRecord::Base
end
# models/director.rb
def Director < Person
end
# models/actor.rb
def Director < Person
end
But because one person can be an actor and an director simultaneously, I want STI with many types like:
person = Person.first
person.type = "Director, Actor"
person.save
Actor.first.id => 1
Director.first.id => 1
Is there mechanism in rails or gem for realize this?
Rails does not support this and I'm not aware of any gems that support this as described (i.e. multiple subclass names in the type column).
There is gem at https://github.com/mhuggins/multiple_table_inheritance which uses separate tables for the subclasses and you can always use mixins as an alternative to inheritance.
I believe the more Rails idiomatic way to do something similar would be via scopes, which would allow you to do:
person = Person.first
person.position = 'Director, Actor'
person.save
person.directors.first.id => 1
person.actors.first.id => 1
And you would just have to define a pair of scopes in your Person class:
scope :actors, -> { where('position like ?', '%Actor%') }
scope :directors, -> { where('position like ?', '%Director%') }
You would lose the ability to do person.is_a? with this, but Ruby doesn't really do multiple inheritance in such a way as to allow #is_a? to return true when passed sibling classes anyway. You can also get effectively similar functionality with a simple test method:
def is_actor?
self.position =~ /Actor/
end
def is_director?
self.position =~ /Director/
end
EDIT: I haven't done a lot of Rails 4, so my scope syntax MAY not be right, I just glanced at the docs. The principle should be sound, though.
Thank to all answerers above!
I found solution that most appropriate for me:
I've created hmt association Person-ProfessionsPerson-Profession and leave descendants for Person class (Director and Actor).
# models/profession.rb
Profession < ActiveRecord::Base
has_many :professions_people, dependent: :destroy
has_many :people, through: :professions_people
end
# models/person.rb
def Person < ActiveRecord::Base
has_many :professions_people, dependent: :destroy
has_many :professions, through: :professions_people
end
# models/director.rb
def Director < Person
include PeopleFromProfession
end
# models/actor.rb
def Actor < Person
include PeopleFromProfession
end
I've seed 2 professions with column "class_type" (which should not change in app's work) "Actor" and "Director"
I've also add concern PeopleFromProfession for share some code:
# models/concerns/actor.rb
module PeopleFromProfession
extend ActiveSupport::Concern
included do
default_scope { includes(:professions).where(professions: {class_type: self.name}) }
after_create :create_join_table_record
end
module ClassMethods
def model_name
Person.model_name
end
end
private
def create_join_table_record
self.professions << Profession.where(class_type: self.class.name).first
end
end
default_scope is for scoping only people with specific profession, create_join_table_record callback is monkey-patch for create missed join table record.
Class method model_name was overwriting for purposes, that covered here Best practices to handle routes for STI subclasses in rails
If you will find some problems in that approach, please tell me.

How to combine two scopes with OR

I have a tag feed and a friend feed.
I want to combine these two and build the ultimate "all" feed.
For friend feed:
class Post < ActiveRecord::Base
scope :friendfeed, lambda{|x| followed_by}
def self.followed_by(user)
where("user_id IN (?) OR user_id = ?", user.watched_ids, user.id)
end
end
For tag feed:
class Post < ActiveRecord::Base
scope :tagfeed, lambda{|x| infatuated_with}
def self.infatuated_with(user)
joins(:attachments).where("attachments.tag_id IN (?)", user.tags).select("DISTINCT pages.*")
end
end
And I would call something like this from the controller (I'm using Kaminari gem for pagination):
#tag_feed = Post.tagfeed(current_user).page(params[:page]).per(21)
#friend_feed = Post.friendfeed(current_user).page(params[:page]).per(21)
Now I want to have a universal feed, but I'm lost. Scopes are meant for narrowing down, but in this case I'm trying to do an OR operation. Doing stuff like
#mother_of_all_feed = #tag_feed + #friend_feed
would be redundant, and I wouldn't be able to control the number of posts appearing on a single page. How can I go about doing this? Thanks!
By the way, for tags I have association set up like this:
class Post < ActiveRecord::Base
has_many :attachments
has_many :tags, :through => :attachments
end
class Tag < ActiveRecord::Base
has_many :attachments
has_many :posts, :through => :attachments
end
class Attachment < ActiveRecord::Base
belongs_to :tag
belongs_to :post
end
There's a rails pull request for this feature (https://github.com/rails/rails/pull/9052), but in the meantime, some one has created a monkey patch that you can include in your initializers that will allow you to or scopes and where clauses in one query and still give you an ActiveRecord::Relation:
https://gist.github.com/j-mcnally/250eaaceef234dd8971b
With that, you'd be able to OR your scopes like this
Post.tagfeed(current_user).or.friendfeed(current_user)
or write a new scope
scope :mother_of_all_feed, lambda{|user| tagfeed(user).or.friendfeed(user)}
Answering my own question. I think I figured out a way.
where("pages.id IN (?) OR pages.id IN (?)",
Page.where(
"user_id IN (?) OR user_id = ?",
user.watched_ids, user.id
),
Page
.joins(:attachments)
.where("attachments.tag_id IN (?)", user.tags)
.select("DISTINCT pages.*")
)
It seems to be working so far, hope this is it!
Here's an example of how I combined two scopes.
scope :reconcilable, -> do
scopes = [
with_matching_insurance_payment_total,
with_zero_insurance_payments_and_zero_amount
]
where('id in (?)', scopes.flatten.map(&:id))
end

Active Relation: Retrieving records through an association?

I have the following models:
class User < ActiveRecord::Base
has_many :survey_takings
end
class SurveyTaking < ActiveRecord::Base
belongs_to :survey
def self.surveys_taken # must return surveys, not survey_takings
where(:state => 'completed').map(&:survey)
end
def self.last_survey_taken
surveys_taken.maximum(:position) # that's Survey#position
end
end
The goal is to be able to call #user.survey_takings.last_survey_taken from a controller. (That's contrived, but go with it; the general goal is to be able to call class methods on #user.survey_takings that can use relations on the associated surveys.)
In its current form, this code won't work; surveys_taken collapses the ActiveRelation into an array when I call .map(&:survey). Is there some way to instead return a relation for all the joined surveys? I can't just do this:
def self.surveys_taken
Survey.join(:survey_takings).where("survey_takings.state = 'completed'")
end
because #user.survey_takings.surveys_taken would join all the completed survey_takings, not just the completed survey_takings for #user.
I guess what I want is the equivalent of
class User < ActiveRecord::Base
has_many :survey_takings
has_many :surveys_taken, :through => :survey_takings, :source => :surveys
end
but I can't access that surveys_taken association from SurveyTaking.last_survey_taken.
If I'm understanding correctly you want to find completed surveys by a certain user? If so you can do:
Survey.join(:survey_takings).where("survey_takings.state = 'completed'", :user => #user)
Also it looks like instead of:
def self.surveys_taken
where(:state => 'completed').map(&:survey)
end
You may want to use scopes:
scope :surveys_taken, where(:state => 'completed')
I think what I'm looking for is this:
class SurveyTaking < ActiveRecord::Base
def self.surveys_taken
Survey.joins(:survey_takings).where("survey_takings.state = 'completed'").merge(self.scoped)
end
end
This way, SurveyTaking.surveys_taken returns surveys taken by anyone, but #user.survey_takings.surveys_taken returns surveys taken by #user. The key is merge(self.scoped).
Waiting for further comments before I accept..

Model Relationship Problem

I am trying to calculate the average (mean) rating for all entries within a category based on the following model associations ...
class Entry < ActiveRecord::Base
acts_as_rateable
belongs_to :category
...
end
class Category < ActiveRecord::Base
has_many :entry
...
end
class Rating < ActiveRecord::Base
belongs_to :rateable, :polymorphic => true
...
end
The rating model is handled by the acts as rateable plugin, so the rateable model looks like this ...
module Rateable #:nodoc:
...
module ClassMethods
def acts_as_rateable
has_many :ratings, :as => :rateable, :dependent => :destroy
...
end
end
...
end
How can I perform the average calculation? Can this be accomplished through the rails model associations or do I have to resort to a SQL query?
The average method is probably what you're looking for. Here's how to use it in your situation:
#category.entries.average('ratings.rating', :joins => :ratings)
Could you use a named_scope or custom method on the model. Either way it would still require some SQL since, if I understand the question, your are calculating a value.
In a traditional database application this would be a view on the data tables.
So in this context you might do something like... (note not tested or sure it is 100% complete)
class Category
has_many :entry do
def avg_rating()
#entries = find :all
#entres.each do |en|
#value += en.rating
end
return #value / entries.count
end
end
Edit - Check out EmFi's revised answer.
I make no promises but try this
class Category
def average_rating
Rating.average :rating,
:conditions => [ "type = ? AND entries.category_id = ?", "Entry", id ],
:join => "JOIN entries ON rateable_id = entries.id"
end
end

Resources