Grouping Query with Has Many Relations in Rails - sql

I have a has_many, through: relationship between Projets and Categories using a Categorization model. A Projet belongs_to a Client.
class Client < ApplicationRecord
has_many :projets
end
class Category < ApplicationRecord
has_many :categorizations, dependent: :destroy
has_many :projets, through: :categorizations
end
class Categorization < ApplicationRecord
belongs_to :category
belongs_to :projet
end
class Projet < ApplicationRecord
belongs_to :client
has_many :categorizations, dependent: :destroy
has_many :categories, through: :categorizations
end
For a specific category I'd like to list all the projets, grouped by the client. e.g.
(for category_id = 3)
Client A
Projet 1
Projet 2
Client B
Projet 3
Client C
Projet 4
So far I can get this working, but only by using two queries (one of which is very inefficient (n+1 problem).
This is the code
def listing
#projets_clients = Projet
.select("client_id")
.includes(:client)
.joins(:categorizations)
.where(categorizations: { category: #category })
.group("client_id")
#clients = []
#projets_clients.each do |p|
#clients << Client.includes(:projets).find(p.client_id)
end
end
If anyone can suggest a better approach I'd love to learn how to optimise this as I haven't been able to find a better way myself.
Thanks.

There are a few different ways to do this. For complex queries I sometimes find it easier to write and execute straight SQL. However for your case, depending on data size, you could just eager load the data and turn it into a hash.
Note: When I was testing this code I used projects instead of projets.
#category = Category.includes(projects: [:client]).find(2)
#projects_by_client = #category.projects.group_by(&:client_id)
# In your view
<%- #projects_by_client.each do |client_id, projects| %>
<%= projects.first.client.name %>
<%- projects.each do |project| %>
<%= project.name %>
<% end %>
<% end %>
A more fleshed out solution might use full sql with a query object and presenter object. I spun up a quick project using the below code and the output is what you are looking for.
# app/controllers/clients_controller.rb
class ClientsController < ApplicationController
def show
result = ClientQuery.call(params[:id])
#presenter = ClientPresenter.new(result)
end
end
# app/services/client_query.rb
class ClientQuery
class << self
def call(client_id)
sql_query(client_id)
end
protected
def sql_query(client_id)
ActiveRecord::Base.
connection.
execute(
sanitized_sql_statement(client_id)
)
end
def sanitized_sql_statement(client_id)
ActiveRecord::Base.send(
:sanitize_sql_array,
[
sql_statement,
client_id
]
)
end
def sql_statement
<<-SQL
SELECT
c.id AS client_id,
c.name AS client_name,
p.name AS project_name
FROM
clients c
INNER JOIN
projects p ON p.client_id = c.id
INNER JOIN
categorizations cz ON cz.project_id = p.id
INNER JOIN
categories ct ON ct.id = cz.category_id
WHERE
ct.id = ?;
SQL
end
end
end
# app/presenters/client_presenter.rb
class ClientPresenter
attr_reader :clients
def initialize(data)
#clients = {}
process_sql_result(data)
end
private
def process_sql_result(data)
data.each do |row|
client_id = row['client_id']
#clients[client_id] ||= { client_name: row['client_name'] }
#clients[client_id][:projects] ||= []
#clients[client_id][:projects] << row['project_name']
end
end
end
# app/views/show.html.erb
<%- #presenter.clients.each do |client_id, client_presenter| %>
<h1><%= client_presenter[:client_name] %></h1>
<ul>
<%- client_presenter[:projects].each do |project_name| %>
<li><%= project_name %></li>
<% end %>
</ul>
<% end %>
This is of course just one of many ways you could go about getting your data in a single query and presenting it.

Related

Load associations to one level while conditionally sideloading associations in Active model serializers

AMS version 0.8.3,
I created a base_serializer.rb like this and extended the same.
class BaseSerializer < ActiveModel::Serializer
def include_associations!
if #options[:embed]
embed = #options[:embed].split(',').map{|item| item.strip.to_sym}
embed.each do |assoc|
include! assoc if _associations.keys.include?(assoc)
end
end
end
end
class EventSerializer < BaseSerializer
attributes :id, :name
has_many :organizers, serializer: OrganizerSerializer
has_many :participants, serializer: ParticipantSerializer
end
class OrganizerSerializer < BaseSerializer
attributes :id, :name
has_many :related, serializer: RelatedSerializer
end
class ParticipantSerializer < BaseSerializer
attributes :id, :name
has_many :related, serializer: RelatedSerializer
end
class RelatedSerializer < BaseSerializer
attributes :id, :name
has_many :something, serializer: SomethingSerializer
end
and the index method in EventsController is written as
# GET /events?embed=organizers,participants
def index
#events = Event.all
render json: #events, embed: params[:embed]
end
With this I can get the :id and :name of events, organizers and participants. But, I want the attributes of related association as well. I don't need details of something serializer. I want to go till this level for each association. How can I achieve that?
I ended up doing this to achieve the same.
class BaseSerializer < ActiveModel::Serializer
def include_associations!
#options[:embed_level] ||= 2
return unless #options.key?(:embed) && #options[:embed_level] != 0
embed = #options[:embed].split(',').map{|item| item.strip.to_sym}
embed.each do |assoc|
next unless _associations.key?(assoc)
assoc_serializer = serializer_for(assoc)
embed = #options[:embed]
embed_level = #options[:embed_level]
#options[:embed_level] = #options[:embed_level] - 1
#options[:embed] = assoc_serializer._associations.keys.join(",")
include! assoc
#options[:embed_level] = embed_level
end
end
def serializer_for(assoc)
serializer = _associations[assoc].options[:serializer]
return serializer if serializer
assoc.to_s.classify.concat("Serializer").constantize
end
end
Ref: Github Issue Link
Special Thanks to Yohan Robert!!!

comma separated string with Advance Search

I've added an Advance Search to my Book App using this tutorial. Everything works fine, but now I am trying to find a Book by its Tags.
I got the advance search to work if the user enters one Tag into the :keywords text_field.
Is there a way to search various tags by splitting the keyword string with commas?
(ex: fun, kid stories, action)
Would allow me to search books with fun OR kids stories OR actions.
How can I search multiple tags via a comma separated string?
Note: I created a search method that I think could help, but I am not sure how to combine it with the single keyword search.
MODEL
class Book < ActiveRecord::Base
has_many :book_mappings
has_many :tags, through: :book_mappings
end
class BookMapping < ActiveRecord::Base
belongs_to :book
belongs_to :tag
end
class Tag < ActiveRecord::Base
has_many :book_mappings
has_many :books, through: :book_mappings
end
class Search < ActiveRecord::Base
def books
#books ||= find_books
end
def find_books
books = Book.order(:name)
###This works for a single word but NOT if I have multiple tags separated by commas
books = books.joins(:tags).where("tags.name like ?", "%#{keywords}%") if keywords.present?
books
end
def search(keywords)
return [] if keywords.blank?
cond_text = keywords.split(', ').map{|w| "name LIKE ? "}.join(" OR ")
cond_values = keywords.split(', ').map{|w| "%#{w}%"}
all(:conditions => (keywords ? [cond_text, *cond_values] : []))
end
end
VIEWS
<%= form_for #search do |f| %>
<div class="field">
<%= f.label :keywords %><br />
<%= f.text_field :keywords %>
</div>
<% end %>
Here is a simple solution. Just add a like statement for each keyword.
To filter books with all the tags
if keywords.present?
books = books.joins(:tags)
keywords.tr(' ','').split(',').each do |keyword|
books = books.where("tags.name like ?", "%#{keyword}%")
end
end
To filter books with any of the tags
if keywords.present?
books = books.joins(:tags)
keyword_names = keywords.split(', ')
cond_text = keyword_names.map{|w| "tags.name like ?"}.join(" OR ")
cond_values = keyword_names.map{|w| "%#{w}%"}
books = books.where(cond_text, *cond_values)
end

Rails: changing from three columns in a table to one

I am modifying a Documents table from using three columns (article1, article2, and article3) to one (articles) which has a string of comma-separated IDs stored in it (i.e., 23,4,33,2). That's all working well, but I'm trying to adjust the functions that read the three columns to read the one and I'm getting rather stuck.
In the model I have:
scope :all_articles, lambda {|p| where(:page => p) }
In the controller I have this:
#articles = (1..3).to_a.map { |i| Article.all_articles(i).reverse }
And in the view:
<% #articles.each_with_index do |a, i| %>
<%= a[i].name %>
<% end %>
It's just a bit beyond me at this point.
Cheers!
It's usually not good practice to put store the ids in a column like you have done. It is better to break that relationship out into a Has and Belongs to Many relationship. You set it up in your models like this:
class Document < ActiveRecord::Base
#...
has_and_belongs_to_many :articles
#...
end
class Article < ActiveRecord::Base
#...
has_and_belongs_to_many :documents
end
Then you will create a join table that ActiveRecord will use to store the relationships.
create_table :articles_documents, :id => false do |t|
t.integer :article_id
t.integer :document_id
end
add_index :articles_documents, [:article_id, :document_id], unique: true
This will allow you to query a lot more efficiently than you are currently doing. For example, to find all documents that have some article id. You would do:
#documents = Document.joins(:articles).where("articles.id = ?", some_article_id)
Or if you want to query for a document and return the articles with it:
#documents = Document.includes(:articles).where(some_conditions)

Rails sorting associations with Ransack

first time poster. I am trying to sort a table of users using the Ransack gem and Kaminari for pagination. When I use name, id, etc. sorting works but when I try an association with posts_count, sorting breaks and won't work. Note: in the view, 'u.posts.count' work correctly. I have tried custom scopes in the users model, and creating custom objects for the search params but nothing seems to work. I think I am having trouble either in the default scope or the #search object not having the data. Need help!
Here are some relevant snippets:
models/user.rb
has_many :posts, :dependent => :destroy
models/post.rb
belongs_to :user
default_scope :order => 'post.created_at DESC'
controllers/users_controller.rb
def index
#title = "User Index"
#search = User.search(params[:q]) # Ransack
#total_users = User.all.count
# .per(10) is the per page for pagination (Kaminari).
#users = #search.result.order("updated_at DESC").page(params[:page]).per(10) #This displays the users that are in the search criteria, paginated.
end
views/users/index.html.erb
..
<%= sort_link #search, :posts_count, "No. of Posts" %> #Sort links at column headers
..
<% #users.each do |u| %> #Display everything in the table
<%= u.posts.count %>
<% end %>
You can add a scope to your User model:
def self.with_posts
joins(:posts).group('posts.id').select('users.*, count(posts.id) as posts_count')
end
and use it like this:
#search = User.with_posts.search(params[:q]) # Ransack
then, you can treat posts_count like any other attribute.
I found a solution:
Controller:
def index
sql = "users.*, (select count(posts.id) from posts\
where posts.user_id = users.id) as count"
#search = User.select(sql).search(params[:q])
if params[:q] && params[:q][:s].include?('count')
#users = #search.result.order(params[:q][:s])
else
#users = #search.result
end
.......
end
View:
<th><%= sort_link #search, :count, "posts count" %></th>

Rails 3: Find parent of polymorphic model in controller?

I'm trying to find an elegant (standard) way to pass the parent of a polymorphic model on to the view. For example:
class Picture < ActiveRecord::Base
belongs_to :imageable, :polymorphic => true
end
class Employee < ActiveRecord::Base
has_many :pictures, :as => :imageable
end
class Product < ActiveRecord::Base
has_many :pictures, :as => :imageable
end
The following way (find_imageable) works, but it seems "hackish".
#PictureController (updated to include full listing)
class PictureController < ApplicationController
#/employees/:id/picture/new
#/products/:id/picture/new
def new
#picture = imageable.pictures.new
respond_with [imageable, #picture]
end
private
def imageable
#imageable ||= find_imageable
end
def find_imageable
params.each do |name, value|
if name =~ /(.+)_id$/
return $1.classify.constantize.find(value)
end
end
nil
end
end
Is there a better way?
EDIT
I'm doing a new action. The path takes the form of parent_model/:id/picture/new and params include the parent id (employee_id or product_id).
I'm not sure exactly what you're trying to do but if you're trying to find the object that 'owns' the picture you should be able to use the imageable_type field to get the class name. You don't even need a helper method for this, just
def show
#picture = Picture.find(params[:id])
#parent = #picture.imagable
#=> so on and so forth
end
Update
For an index action you could do
def index
#pictures = Picture.includes(:imagable).all
end
That will instantiate all 'imagables' for you.
Update II: The Wrath of Poly
For your new method you could just pass the id to your constructor, but if you want to instantiate the parent you could get it from the url like
def parent
#parent ||= %w(employee product).find {|p| request.path.split('/').include? p }
end
def parent_class
parent.classify.constantize
end
def imageable
#imageable ||= parent_class.find(params["#{parent}_id"])
end
You could of course define a constant in your controller that contained the possible parents and use that instead of listing them in the method explicitly. Using the request path object feels a little more 'Rails-y' to me.
I just ran into this same problem.
The way I 'sort of' solved it is defining a find_parent method in each model with polymorphic associations.
class Polymorphic1 < ActiveRecord::Base
belongs_to :parent1, :polymorphic => true
def find_parent
self.parent1
end
end
class Polymorphic2 < ActiveRecord::Base
belongs_to :parent2, :polymorphic => true
def find_parent
self.parent2
end
end
Unfortunately, I can not think of a better way. Hope this helps a bit for you.
This is the way I did it for multiple nested resources, where the last param is the polymorphic model we are dealing with: (only slightly different from your own)
def find_noteable
#possibilities = []
params.each do |name, value|
if name =~ /(.+)_id$/
#possibilities.push $1.classify.constantize.find(value)
end
end
return #possibilities.last
end
Then in the view, something like this:
<% # Don't think this was needed: #possibilities << picture %>
<%= link_to polymorphic_path(#possibilities.map {|p| p}) do %>
The reason for returning the last of that array is to allow finding the child/poly records in question i.e. #employee.pictures or #product.pictures