I have 2 tables : entradas (id, blah...) and georelaciones (:entrada_id, lugar_id, blah...). georelaciones is just a link table for a polymorphic relation.
I want to retrieve all the entradas which are in relation with each lugar_id (pass in an array).
I have build this ugly scope ("d" will be an array of lugar_id):
scope :con_pais, lambda {|d|
# select e.id from entradas as e inner join georelaciones as g1 on (g1.lugar_id = 55 and g1.entrada_id = e.id) inner join georelaciones as g2 on (g2.lugar_id = 66 and g2.entrada_id = e.id)
cadena = ""
i = 0
d.each{ |lugar_id| i += 1 ; cadena << " inner join georelaciones as g" + i.to_s + " on (g"+ i.to_s + ".lugar_id = " + lugar_id.to_s + " and g" + i.to_s + ".entrada_id = e.id)"}
entradas = Entrada.find_by_sql("select distinct e.id from entradas as e " + cadena)
a = []
entradas.each{ |e| a << e.id }
Entrada.where("id in (?)", a)
}
I know that's not good because my table entradas has some million records and I do not take advantage of lazy loading due to the use of find_by_sql.
How can I query the database and return an ActiveRecord::Relation directly without using find_by_sql?
Entrada.find_by_sql("select distinct e.id from entradas as e " + cadena)
=>
Entrada.select("distinct e.id as e #{cadena}")
Assuming that I got your relations correct, you should be able to do something like this:
class Georelacion < ActiveRecord::Base
belongs_to :entrada
scope :con_pais, lambda { |d|
includes(:entrada).where("georelaciones.lugar_id IN (?)", d)
}
...
end
And then in the controller:
#lugar_ids = [1,2,3,4]
#georelaciones = Georelacion.con_pais(#lugar_ids).group_by(&:lugar_id)
That should give you a hash where each key is a lugar_id and the value for each key is an array of georelaciones with the associated entrada already loaded from the database.
Related
I am trying to process the results of an array into a string to pass for a search. I want to build a string from the array that would look something like
("categories.name like '%Forms%' or categories.name like '%Apples%'")
serialize :category, JSON
if category.count > 1 && category.index != 0
$search_global.category.each do |cat_name|
cat_name.slice '" '
# cat_name
$array_count = $array_count + 1
if cat_name != ''
$inside_count = $inside_count +1
$cat_name_2 = "categories.name like %" + $cat_name_2 + cat_name + "% or " + $inside_count.to_s
end
end
end
If I select one item, it works fine as in
categories.name like %Forms% or 1
Please note that I am including the inside count just to get a better idea of what is happening.
The problem I have is when I select 2 or more items. categories.name like % is repeated twice and then the array items or listed as in
categories.name like %categories.name like %Calendar% or 1Forms% or 2
I can't seem to figure out why the concatenation isn't working as I expected.
$cat_name_2 = "categories.name like %" + $cat_name_2 + cat_name + "% or " + $inside_count.to_s
Your are using $cat_name_2 as the asignee as well as inside the assignment statement.
s is hashes of array
relativebase = s.pluck(:base_point).inject(:+) + s.pluck(:distance_point).inject(:+) + s.pluck(:speed_point).inject(:+) + s.pluck(:frequency_point).inject(:+) + s.pluck(:quality_point).inject(:+)
This is calling the database four times which I want to do in one single query. How can i get this.
Something like:
User.select(:a, :b, :c, :d).all.inject([]) { |res, e| res << e.a; res << e.b; res << e.c; res << e.d; res }
Rails 4 supports multiple names in pluck:
relativebase = s.pluck(:base_point,
:distance_point,
:speed_point,
:frequency_point).inject(0) do |sum, (bp, dp, sp, fp)|
sum + bp + dp + sp + fp
end
Actually I personally think that it is probably better to calculate the sum in the database, but I'm not sure how to do that elegantly in rails:
SELECT SUM(base_point + distance_point + speed_point + frequency_point) FROM s
I have this query:
SELECT f.id,
Concat(f.name, ' ', REPLACE(f.parent_names, ',', ' ?')) AS FullName,
u.name AS Unit,
u.id AS UnitId,
u.position AS UnitPosition,
city.name AS City,
hus.mobile1 AS HusMobile,
wife.mobile1 AS WifeMobile,
hus.first_name AS Spouse1,
wife.first_name AS Spouse2,
f.phone AS HomePhone,
f.email AS Email,
Date_format(f.contact_initiation_date, '%d/%m/%Y') AS InitDate,
f.contact_initiation_date AS sql_date,
Date_format(f.status_change_date, '%d/%m/%Y') AS StatususChangeDate,
stts.name AS 'Status',
(SELECT Group_concat(' ', t.name, '<', t.id, '>' ORDER BY t.position DESC
)
FROM taggings tgs
JOIN tags t
ON tgs.tag_id = t.id
WHERE tgs.taggable_type = 'family'
AND tgs.taggable_id = (SELECT DISTINCT taggable_id
FROM taggings
WHERE tag_id IN( 76, 72, 74 )
AND taggable_id = f.id)) AS HandlingStatus,
Date_format(f.reconnection_date, '%d/%m/%Y') AS ReconnectionDate,
f.reconnection_date AS reconnection_sql_date,
Date_format(cmt.created_at, '%d/%m/%Y') AS CommentDate,
cmt.comment AS LastComment,
usr.name AS Comentator,
Format(e.income_total, 0) AS Income,
Format(e.expense_total, 0) AS Expense,
Format(e.debts_total, 0) AS Debts
FROM families f
JOIN categories stts
ON f.family_status_cat_id = stts.id
JOIN units u
ON f.unit_id = u.id
JOIN categories city
ON f.main_city_cat_id = city.id
LEFT JOIN comments cmt
ON f.last_comment_id = cmt.id
LEFT JOIN contacts hus
ON hus.id = f.husband_id
LEFT JOIN contacts wife
ON wife.id = f.wife_id
LEFT JOIN users usr
ON usr.id = cmt.user_id
LEFT JOIN escort_requests e
ON e.family_id = f.id
WHERE ( 1 = 0
OR ( u.is_busy = 0
AND f.family_status_cat_id = 1421 )
OR ( u.is_busy = 1
AND f.family_status_cat_id = 1421 )
OR ( f.family_status_cat_id = 1423 )
OR ( f.family_status_cat_id = 1424 ) )
HAVING ( 1 = 1
AND handlingstatus IS NOT NULL
AND fullname LIKE '%%' )
ORDER BY fullname
LIMIT 0, 100
How do I write it in Rails? Right now I build an array with a string, but is there a rails way to do it better? If there is no better way, just mention it.
The values in the line: WHERE tag_id IN( 76, 72, 74 ) are being created dynamically.
I've been trying to integrate some complex SQL with Rails, and found no better way than to run the SQL directly. Anything that strays away from a very simple query can get very difficult to code (and understand) in activerecord.
In my model I have a couple of queries that can be used (and re-used) one after another. One of those should aggregate amounts. This works fine on SQLite and throws an error on Postgres:
ActiveRecord::StatementInvalid (PGError: ERROR: column "entries.date" must appear in the GROUP BY clause or be used in an aggregate function
: SELECT sum(case when joint = false then amount else amount / 2 end) as total, sum(case when joint = false then amount else 0 end) as sum_personal, sum(case when joint = true and user_id = 1 then amount / 2 else 0 end) as sum_user_joint, sum(case when joint = true and user_id = 2 then amount / 2 else 0 end) as sum_partner_joint FROM "entries" WHERE (1 = entries.user_id OR (2 = entries.user_id AND entries.joint = 't')) AND ('2011-04-01' <= entries.date AND entries.date <= '2011-04-30') AND (amount_calc > 0 AND compensation = 'f') ORDER BY date asc)
Relevant part of Model.rb
# all entries of one month
def self.all_entries_month(year, month, user_id, partner_id)
mydate = Date.new(year, month, 1)
where(':user_id = entries.user_id OR (:partner_id = entries.user_id AND entries.joint = :true)', {
:user_id => user_id,
:partner_id => partner_id,
:true => true
}).
where(':first_day <= entries.date AND entries.date <= :last_day', {
:first_day => mydate,
:last_day => mydate.at_end_of_month
})
end
def self.income
where('amount_calc > 0 AND compensation = ?', false)
end
def self.cost
where('amount_calc <= 0 AND compensation = ?', false)
end
def self.order_by_date
order('date asc')
end
# group by tag and build sum of groups named group_sum
def self.group_by_tag(order)
group('tag').
select('tag, ' +
'sum(case when joint = "f" then amount else amount / 2 end) as tag_sum'
).
order('tag_sum ' + order)
end
def self.multiple_sums(user_id, partner_id)
case ActiveRecord::Base.connection.adapter_name
when 'SQLite'
select('sum(case when joint = "f" then amount else amount / 2 end) as total, ' +
'sum(case when joint = "f" then amount else 0 end) as sum_personal, ' +
'sum(case when joint = "t" and user_id = ' + user_id.to_s + ' then amount / 2 else 0 end) as sum_user_joint, ' +
'sum(case when joint = "t" and user_id = ' + partner_id.to_s + ' then amount / 2 else 0 end) as sum_partner_joint '
)
when 'PostgreSQL'
select('sum(case when joint = false then amount else amount / 2 end) as total, ' +
'sum(case when joint = false then amount else 0 end) as sum_personal, ' +
'sum(case when joint = true and user_id = ' + user_id.to_s + ' then amount / 2 else 0 end) as sum_user_joint, ' +
'sum(case when joint = true and user_id = ' + partner_id.to_s + ' then amount / 2 else 0 end) as sum_partner_joint '
)
else
raise 'Query not implemented for this DB adapter'
end
end
Controller
# get all entries of given month
#cost = Entry.all_entries_month(#year, #month, current_user.id, current_partner.id).cost
# group cost by categories
#group_cost = #cost.group_by_tag('asc')
# still need to sort by date
#cost = #cost.order_by_date
#calc_cost = #cost.multiple_sums(current_user.id, current_partner.id)[0]
How can I change my query multiple_sums without breaking the other queries? Or do I need to implement multiple_sums from ground without using the existing ones?
Remove order by clause, which is useless as far as I can see anyway because you're grouping into single row.
And please - reformat your queries so that they will be visible and readable.
Heroku throws an error on my Postgres-Query stating:
ActiveRecord::StatementInvalid
(PGError: ERROR: syntax error at or
near "date" 2011-05-03T13:58:22+00:00
app[web.1]: LINE 1: ...2011-05-31')
GROUP BY EXTRACT(YEAR FROM TIMESTAMP
date)||EXT...
The SQLite query in development works as expected. Here is the code:
def self.calculate(year, month, user_id, partner_id)
case ActiveRecord::Base.connection.adapter_name
when 'SQLite'
where(':user_id = entries.user_id OR :partner_id = entries.user_id', {
:user_id => user_id,
:partner_id => partner_id
}).
where('entries.date <= :last_day', {
:last_day => Date.new(year, month, 1).at_end_of_month
}).
select('entries.date, ' +
'sum(case when joint = "f" then amount_calc else 0 end) as sum_private, ' +
'sum(case when joint = "t" and user_id = ' + user_id.to_s + ' then amount_calc else 0 end) as sum_user_joint, ' +
'sum(case when joint = "t" and user_id = ' + partner_id.to_s + ' then amount_calc else 0 end) as sum_partner_joint, ' +
'sum(case when compensation = "t" and user_id = ' + user_id.to_s + ' then amount_calc else 0 end) as sum_user_compensation, ' +
'sum(case when compensation = "t" and user_id = ' + partner_id.to_s + ' then amount_calc else 0 end) as sum_partner_compensation '
).
group("strftime('%Y-%m', date)")
when 'PostgreSQL'
where(':user_id = entries.user_id OR :partner_id = entries.user_id', {
:user_id => user_id,
:partner_id => partner_id
}).
where('entries.date <= :last_day', {
:last_day => Date.new(year, month, 1).at_end_of_month
}).
select('entries.date, ' +
'sum(case when joint = "f" then amount_calc else 0 end) as sum_private, ' +
'sum(case when joint = "t" and user_id = ' + user_id.to_s + ' then amount_calc else 0 end) as sum_user_joint, ' +
'sum(case when joint = "t" and user_id = ' + partner_id.to_s + ' then amount_calc else 0 end) as sum_partner_joint, ' +
'sum(case when compensation = "t" and user_id = ' + user_id.to_s + ' then amount_calc else 0 end) as sum_user_compensation, ' +
'sum(case when compensation = "t" and user_id = ' + partner_id.to_s + ' then amount_calc else 0 end) as sum_partner_compensation '
).
group("EXTRACT(YEAR FROM TIMESTAMP date)||EXTRACT(MONTH FROM TIMESTAMP date)")
else
raise 'Query not implemented for this DB adapter'
end
end
I would really appreciate any hints. And as I am already asking a question here, I am uncertain about the case when joint = "t" in the sums in both queries too, is there a better way to do this?
UPDATE
Thanks to both peufeu and a horse with no name the code now looks like:
when 'PostgreSQL'
where(':user_id = entries.user_id OR :partner_id = entries.user_id', {
:user_id => user_id,
:partner_id => partner_id
}).
where('entries.date <= :last_day', {
:last_day => Date.new(year, month, 1).at_end_of_month
}).
select('min(entries.date) as date, ' +
'sum(case when joint = false then amount_calc else 0 end) as sum_private, ' +
'sum(case when joint = true and user_id = ' + user_id.to_s + ' then amount_calc else 0 end) as sum_user_joint, ' +
'sum(case when joint = true and user_id = ' + partner_id.to_s + ' then amount_calc else 0 end) as sum_partner_joint, ' +
'sum(case when compensation = true and user_id = ' + user_id.to_s + ' then amount_calc else 0 end) as sum_user_compensation, ' +
'sum(case when compensation = true and user_id = ' + partner_id.to_s + ' then amount_calc else 0 end) as sum_partner_compensation '
).
group('EXTRACT(YEAR FROM "date"), EXTRACT(MONTH FROM "date")')
...and works like expected.
Another of my statement runs into troubles now and I edit it here as it seems related to the answer of peufeu. Model/Controller:
def self.all_entries_month(year, month, user_id, partner_id)
mydate = Date.new(year, month, 1)
where(':user_id = entries.user_id OR (:partner_id = entries.user_id AND entries.joint = :true)', {
:user_id => user_id,
:partner_id => partner_id,
:true => true
}).
where(':first_day <= entries.date AND entries.date <= :last_day', {
:first_day => mydate,
:last_day => mydate.at_end_of_month
})
end
# group by tag and build sum of groups named group_sum
def self.group_by_tag
group('tag').
select('entries.*, sum(amount_calc) as group_sum')
end
controller:
#income = Entry.all_entries_month(#year, #month, current_user.id, current_partner.id).income
#cost = Entry.all_entries_month(#year, #month, current_user.id, current_partner.id).cost
# group cost by categories
#group_income = #income.group_by_tag.order('group_sum desc')
#group_cost = #cost.group_by_tag.order('group_sum')
The error is:
ActionView::Template::Error (PGError: ERROR: column "entries.id" must appear in the GROUP BY clause or be used in an aggregate function
2011-05-03T18:35:20+00:00 app[web.1]: : SELECT entries.*, sum(amount_calc) as group_sum FROM "entries" WHERE (1 = entries.user_id OR (2 = entries.user_id AND entries.joint = 't')) AND ('2011-04-01' <= entries.date AND entries.date <= '2011-04-30') AND (amount_calc <= 0 AND compensation = 'f') GROUP BY tag ORDER BY group_sum):
2011-05-03T18:35:20+00:00 app[web.1]: 6: </thead>
2011-05-03T18:35:20+00:00 app[web.1]: 7: <tbody>
2011-05-03T18:35:20+00:00 app[web.1]: 8: <% if categories %>
2011-05-03T18:35:20+00:00 app[web.1]: 9: <% categories.each do |category| %>
2011-05-03T18:35:20+00:00 app[web.1]: 10: <tr>
2011-05-03T18:35:20+00:00 app[web.1]: 11: <td class="align-left"><%= category.tag %></td>
2011-05-03T18:35:20+00:00 app[web.1]: 12: <td class="align-right"><%= my_number_to_percentage (category.group_sum.to_f / total_cost) * 100 %></td>
UPDATE 2: I found the solution
# group by tag and build sum of groups named group_sum
def self.group_by_tag
group('tag').
select('tag, sum(amount_calc) as group_sum')
end
Problem is there :
EXTRACT(YEAR FROM TIMESTAMP date)||EXTRACT(MONTH FROM TIMESTAMP date)
Solution :
EXTRACT(YEAR FROM "date"), EXTRACT(MONTH FROM "date")
The word "TIMESTAMP" is a bit out of place there ;) ... also :
Since DATE is a reserved SQL keyword, it is a very bad idea to use it as a column name. Here, I quoted it using " so postgres doesn''t get confused.
And you don't need to waste CPU time building a string concatenating your two EXTRACTs, just remember GROUP BY can use several parameters.
Then of course the query will fail because you SELECT "date" but your dont' GROUP BY date. postgres has no way to know which date you want from all the rows which have the same Year-Month. MySQL will return a value at random from the rows, postgres likes correctness so it will throw an error. You could SELECT min(date) for instance, which would be correct.
I am uncertain about the case when joint = "t"
That depends on how you want to get your results, nothing wrong there.
Maybe using several queries would scan a smaller portion of the table (I don't know about your dataset) or maybe not.
I don't know ruby/heroku, but the expression
joint = "t"
refers to a column t in PostgreSQL because object names are quoted with double quotes. So unless Ruby/Heroku is replacing those double quotes with single quotes that will be an invalid condition.
If t should be string literal (the character t) then you need to use single quotes: joint = 't'
If joint is of type boolean then you should use joint = true in PostgreSQL