Problem I'm trying to solve:
Multiple projects connect/make updates to the same DB. "Project A" is tasked with making updates to the DB, "Project B" is the client interface, tasked with showing updated data.
Solution:
When Project A is scheduled to update records, Project B will create a background Job to run for 15 minutes to catch the updates made, updating the interface every 15 seconds via ActionCable (webhooks).
Issue:
ActiveJob is stuck on "3 remaining records" without noticing any DB changes. If the Model is called directly (outside of the Job), everything works as expected; DB changes are noticed, client interface gets updated, and everyone is happy.
But as soon as it's called from the Job (to run the task in the background), it gets stuck.
Job that calls the Model logic:
class BatchWatchJob < ApplicationJob
queue_as :default
def perform(**args)
if args[:batch_number]
p "=== Begin - BatchWatch for batch_number: #{args[:batch_number]} ==="
begin
# Set 15 minute timeout
# ReImport.watch_batch() not updating
# it's count when DB data changes :/
# BUT if I call the method directly (not from this Job), it works
Timeout.timeout(900) { ReImport.watch_batch(args[:batch_number]) }
rescue Timeout::Error
puts '=== TIMEOUT ERROR ==='
puts 'BatchWatch job failed to finish in the requisite amount of time (15 minutes)'
end
else
p "=== Unable to start BatchWatch, batch_number not specified ==="
end
end
end
Model containing my logic:
class ReImport < ApplicationRecord
[...]
def self.watch_batch(num)
batch = ReImport.where(batch: num)
total = batch.count
remaining = batch.where(completed: false) # NOTE: starts with 3 remaining
remaining_count = remaining.count
p "initial remaining: #{remaining_count}"
while !remaining.empty? do
p 'wait 10 seconds...'
sleep(10) # wait 10 seconds
p '...seconds finished...'
# batch.reload # doesn't work
# remaining.reload # doesn't work
batch = ReImport.where(batch: num)
remaining = batch.where(completed: false) # NOTE: this should now be 2
p "remaining_count: #{remaining_count}"
p "remaining . count: #{remaining.count}"
p "(remaining_count > remaining.count): #{(remaining_count > remaining.count)}"
if remaining_count > remaining.count
p '=== WATCH_BATCH: update found! ==='
# Update count
remaining_count = remaining.count
# Broadcast DataTables to update records
ReImportBatchChannel.broadcast_to("re_import_batch_#{num}", {update: true})
else
p '=== WATCH_BATCH: nothing changed yet ==='
end
end
p '=== WATCH_BATCH COMPLETED (or timeout reached) ==='
end
[...]
end
Rails Console - Output:
> BatchWatchJob.perform_later(batch_number: 7)
Enqueued BatchWatchJob (Job ID: 7d1beeaf-c4d8-4489-885c-13b44d1037cf) to Async(default) with arguments: {:batch_number=>7}
=> #<BatchWatchJob:0x000000055c2100 #arguments=[{:batch_number=>7}], #job_id="7d1beeaf-c4d8-4489-885c-13b44d1037cf", #queue_name="default", #priority=nil, #executions=0, #provider_job_id="011e004c-34f5-4c7a-9925-052df1aa1774">
Performing BatchWatchJob (Job ID: 7d1beeaf-c4d8-4489-885c-13b44d1037cf) from Async(default) with arguments: {:batch_number=>7}
"=== Begin - BatchWatch for batch_number: 7 ==="
(2.1ms) SET ##SESSION.sql_mode = CONCAT(CONCAT(##sql_mode, ',STRICT_ALL_TABLES'), ',NO_AUTO_VALUE_ON_ZERO'), ##SESSION.sql_auto_is_null = 0, ##SESSION.wait_timeout = 2147483
(0.9ms) SELECT COUNT(*) FROM `re_imports` WHERE `re_imports`.`batch` = 7
(0.7ms) SELECT COUNT(*) FROM `re_imports` WHERE `re_imports`.`batch` = 7 AND `re_imports`.`completed` = 0
"initial remaining: 3"
ReImport Exists (0.5ms) SELECT 1 AS one FROM `re_imports` WHERE `re_imports`.`batch` = 7 AND `re_imports`.`completed` = 0 LIMIT 1
"wait 10 seconds..."
"...seconds finished..."
"remaining_count: 3"
CACHE (0.0ms) SELECT COUNT(*) FROM `re_imports` WHERE `re_imports`.`batch` = 7 AND `re_imports`.`completed` = 0 [["batch", 7], ["completed", 0]]
"remaining . count: 3"
CACHE (0.0ms) SELECT COUNT(*) FROM `re_imports` WHERE `re_imports`.`batch` = 7 AND `re_imports`.`completed` = 0 [["batch", 7], ["completed", 0]]
"(remaining_count > remaining.count): false"
CACHE (0.0ms) SELECT COUNT(*) FROM `re_imports` WHERE `re_imports`.`batch` = 7 AND `re_imports`.`completed` = 0 [["batch", 7], ["completed", 0]]
"=== WATCH_BATCH: nothing changed yet ==="
CACHE ReImport Exists (0.0ms) SELECT 1 AS one FROM `re_imports` WHERE `re_imports`.`batch` = 7 AND `re_imports`.`completed` = 0 LIMIT 1 [["batch", 7], ["completed", 0], ["LIMIT", 1]]
"wait 10 seconds..."
"...seconds finished..."
"remaining_count: 3"
CACHE (0.0ms) SELECT COUNT(*) FROM `re_imports` WHERE `re_imports`.`batch` = 7 AND `re_imports`.`completed` = 0 [["batch", 7], ["completed", 0]]
"remaining . count: 3"
CACHE (0.0ms) SELECT COUNT(*) FROM `re_imports` WHERE `re_imports`.`batch` = 7 AND `re_imports`.`completed` = 0 [["batch", 7], ["completed", 0]]
"(remaining_count > remaining.count): false"
CACHE (0.0ms) SELECT COUNT(*) FROM `re_imports` WHERE `re_imports`.`batch` = 7 AND `re_imports`.`completed` = 0 [["batch", 7], ["completed", 0]]
"=== WATCH_BATCH: nothing changed yet ==="
CACHE ReImport Exists (0.0ms) SELECT 1 AS one FROM `re_imports` WHERE `re_imports`.`batch` = 7 AND `re_imports`.`completed` = 0 LIMIT 1 [["batch", 7], ["completed", 0], ["LIMIT", 1]]
"wait 10 seconds..."
[...]
Performed BatchWatchJob (Job ID: 7d1beeaf-c4d8-4489-885c-13b44d1037cf) from Async(default) in 22863.44ms
I'm not just noticing CACHE ReImport Exists, trying to figure out how to turn that off...
I found that the problem is SQL Caching, and fixed it by using ActiveRecords uncached method ReImport.uncached do within the while loop like so:
Model
class ReImport < ApplicationRecord
[...]
def self.watch_batch(num)
batch = ReImport.where(batch: num)
total = batch.count
remaining = batch.where(completed: false)
remaining_count = remaining.count
p "initial remaining: #{remaining_count}"
while !remaining.empty? do
ReImport.uncached do # <--- UNCACHE SQL QUERIES
p 'wait 10 seconds...'
sleep(10) # wait 10 seconds
p '...seconds finished...'
batch = ReImport.where(batch: num)
remaining = batch.where(completed: false)
p "remaining_count: #{remaining_count}"
p "remaining . count: #{remaining.count}"
p "(remaining_count > remaining.count): #{(remaining_count > remaining.count)}"
if remaining_count > remaining.count
p '=== WATCH_BATCH: update found! ==='
# Update count
remaining_count = remaining.count
# Broadcast DataTables to update records
ReImportBatchChannel.broadcast_to("re_import_batch_#{num}", {update: true})
else
p '=== WATCH_BATCH: nothing changed yet ==='
end
end
end
p '=== WATCH_BATCH COMPLETED (or timeout reached) ==='
end
[...]
end
Source
Related
I have a method that computes stats (mainly sums) on a number of float attributes in a model.
The models
class GroupPlayer < ActiveRecord::Base
belongs_to :group
has_many :scored_rounds
has_many :rounds, dependent: :destroy
end
class Round < ActiveRecord::Base
belongs_to :group_player
end
class ScoredRound < Round
# STI
end
The method that provides stats on up to 4 float attributes that is called from a other methods, depending if I'm getting stats for one player or a group of players. An initial filter on ScoredRound is passed to the method (sr)
def method_stats(method,sr,grp)
rounds = sr.where.not(method => nil)
number_rounds = rounds.count
won = rounds.sum(method).round(2)
if method == :quality
dues = grp.options[:dues] * number_rounds
else
dues = grp.options["#{method.to_s}_dues"] * number_rounds
end
balance = (won - dues).round(2)
perc = dues > 0 ? (won / dues).round(3) : 0.0
[self.full_name,number_rounds,won,dues,balance,perc]
end
3 of the 4 attributes I am summing in ScoredRounds may not be set (nil) if the player did not win that game so the rounds are filtered.
Everything worked fine until I decided to add a limit on how many rounds to use. For instance if I only wanted status for the last 25 rounds in the query passed to method_stats I'd call:
def money_stats(grp,method,limit=100)
sr = self.scored_rounds.where.not(method => nil).order(:date).reverse_order.limit(limit)
method_stats(method,sr,grp)
end
Again, I just added the limit and order clause to the query. Worked fine for all records.
If I simulate the procedure in the console with out using the above methods (or using them!) I'll get an erroneous sum
gp = GroupPlayer.find(123)
GroupPlayer Load (2.1ms) SELECT "group_players".* FROM "group_players" WHERE "group_players"."id" = $1 LIMIT $2 [["id", 123], ["LIMIT", 1]]
=> valid group player
sr = gp.scored_rounds.where.not(:quality => nil)
ScoredRound Load (1.7ms) SELECT "rounds".* FROM "rounds" WHERE "rounds"."type" IN ('ScoredRound') AND "rounds"."group_player_id" = $1 AND ("rounds"."quality" IS NOT NULL) [["group_player_id", 123]]
=> #<ActiveRecord::AssociationRelation [#<ScoredRound id: 5706, player_id: 123, group_player_id: 123, event_id: 12, type: "ScoredRound", date: "2016-11-04", team: 3, tee: "White", quota: 32, front: 15, back: 15, total: 30, created_at: "2016-11-04 14:18:27", updated_at: "2016-11-04 19:12:47", quality: 0.0, skins: nil, par3: nil, other: nil>,...]
sr.count
(1.5ms) SELECT COUNT(*) FROM "rounds" WHERE "rounds"."type" IN ('ScoredRound') AND "rounds"."group_player_id" = $1 AND ("rounds"."quality" IS NOT NULL) [["group_player_id", 123]]
=> 44
sr.sum(:quality)
(1.0ms) SELECT SUM("rounds"."quality") FROM "rounds" WHERE "rounds"."type" IN ('ScoredRound') AND "rounds"."group_player_id" = $1 AND ("rounds"."quality" IS NOT NULL) [["group_player_id", 123]]
=> 354.166666666667
# Now if I add the order and limit clause
sr = gp.scored_rounds.where.not(:quality => nil).order(:date).reverse_order.limit(25)
ScoredRound Load (1.6ms) SELECT "rounds".* FROM "rounds" WHERE "rounds"."type" IN ('ScoredRound') AND "rounds"."group_player_id" = $1 AND ("rounds"."quality" IS NOT NULL) ORDER BY "rounds"."date" DESC LIMIT $2 [["group_player_id", 123], ["LIMIT", 25]]
=> => #<ActiveRecord::AssociationRelation [...]
sr.count
(1.1ms) SELECT COUNT(count_column) FROM (SELECT 1 AS count_column FROM "rounds" WHERE "rounds"."type" IN ('ScoredRound') AND "rounds"."group_player_id" = $1 AND ("rounds"."quality" IS NOT NULL) LIMIT $2) subquery_for_count [["group_player_id", 123], ["LIMIT", 25]]
=> 25
sr.sum(:quality)
(1.8ms) SELECT SUM("rounds"."quality") FROM "rounds" WHERE "rounds"."type" IN ('ScoredRound') AND "rounds"."group_player_id" = $1 AND ("rounds"."quality" IS NOT NULL) LIMIT $2 [["group_player_id", 123], ["LIMIT", 25]]
=> 354.166666666667
### This is the error, it return the sum off all records,
# not the limited???? if I use pluck and sum
sr.pluck(:quality)
=> [10.0, 11.3333333333333, 10.0, 34.0, 0.0, 7.33333333333333, 0.0, 0.0, 31.5, 0.0, 21.3333333333333, 0.0, 19.0, 0.0, 0.0, 7.5, 0.0, 20.0, 10.0, 28.0, 8.0, 9.5, 0.0, 3.0, 24.0]
sr.pluck(:quality).sum
=> 254.49999999999994
Don't know if I found a bug in AREL or I'm doing something wrong. I tried it with just Round instead of the STI ScoredRound with the same results.
Any ideas?
If you notice, the SUM results for both, with and without LIMIT, are the same:
sr = gp.scored_rounds.where.not(:quality => nil)
sr.sum(:quality)
(1.0ms) SELECT SUM("rounds"."quality") FROM "rounds" WHERE "rounds"."type" IN ('ScoredRound') AND "rounds"."group_player_id" = $1 AND ("rounds"."quality" IS NOT NULL) [["group_player_id", 123]]
=> 354.166666666667
sr = gp.scored_rounds.where.not(:quality => nil).order(:date).reverse_order.limit(25)
sr.sum(:quality)
(1.8ms) SELECT SUM("rounds"."quality") FROM "rounds" WHERE "rounds"."type" IN ('ScoredRound') AND "rounds"."group_player_id" = $1 AND ("rounds"."quality" IS NOT NULL) LIMIT $2 [["group_player_id", 123], ["LIMIT", 25]]
=> 354.166666666667
That's because LIMIT affects the number of rows returned by the query and SUM returns just one, so the function is applied for all the 44 records, not the 25 given to LIMIT. That's not what happens with sr.pluck(:quality).sum which applies only to the 25 records returned by the query.
Don't know if I found a bug in AREL or I'm doing something wrong
Sadly, 99.9% of times is not a bug but our fault :(
# File activerecord/lib/active_record/relation/calculations.rb, line 75
def sum(column_name = nil)
return super() if block_given?
calculate(:sum, column_name)
end
if you call sr.sum(:quality) then sum take quality as a column name and Calculates the sum of values on a given column.
Have a collection of categories, represented as collections of objects. Each object has a sort property. This property takes a numeric value from 1 to the number which is the last element of the collection. There is a list of categories filtered by the sort property
linecategories=Linecategory.eager_load(:main_image).order('sort')
Then this list is dragged drag and drap and a certain number of objects to the collection is changing the value of this property sort. Further ajax goes from 2 to n of objects whose properties of the sort has changed.
Task to do update the changed fields in the database.
Now data comes in the form of:
data={}
data['items']=params[:data]
Where data has the form
data: [{id: 1, name: "Гороскопы", slug: "horoscopes", title: "keywords-horoscopes",…},…]
0: {id: 1, name: "Гороскопы", slug: "horoscopes", title: "keywords-horoscopes",…}
created_at: "2015-01-10T21:14:56.000Z"
description: "description-horoscopes"
id: 1
keywords: "keywords-horoscopes"
name: "Гороскопы"
slug: "horoscopes"
sort: 1
title: "keywords-horoscopes"
updated_at: "2015-07-19T19:10:03.000Z"
1: {id: 5, name: "Гадания", slug: "divination", title: "eywords-divination",…}
created_at: "2015-01-11T08:47:10.000Z"
description: "2015-01-11 08:47:10"
id: 5
keywords: "description-divination"
main_image: {id: 1, src: "images/categories_images/devination.jpg", linecategory_id: 5, created_at: null,…}
name: "Гадания"
slug: "divination"
sort: 2
title: "eywords-divination"
updated_at: "2015-07-19T19:24:23.000Z"
2: {id: 3, name: "Вкусности", slug: "delicious", title: "keywords-delicious",…}
created_at: "2015-01-10T21:17:28.000Z"
description: "2015-01-10 21:17:28"
id: 3
keywords: "description-delicious"
name: "Вкусности"
slug: "delicious"
sort: 3
title: "keywords-delicious"
updated_at: "2015-07-19T19:24:23.000Z"
Now I implemented the update like so:
def updates
data={}
data['items']=params[:data]
data['items'].each { |el|
#item=Linecategory.find(el['id'])
#item.update_attributes(el.permit(:sort))
}
render json: {update: 1, data: data['items']}
end
But I think it is not the optimal solution, I think it can be done more efficiently and more beautiful by means of ruby on rails and activerecords whether this is so, and if possible suggest solutions to the problem?
def updates
data=params[:data].as_json(only: [:id, :sort])
h = {}
data.each_with_index { |e, i| h[e['id']] = e }
Linecategory.update(h.keys, h.values)
render json: {update: 1, data: h}
end
Mine has found solutions, I still don't katsa completely optimal, since before the update I have to use:
data.each_with_index { |e, i| h[e['id']] = e }
but even so it works.
Plausible queries to update the database continues to be set, but girlfriend can do it?
Linecategory Load (0.4ms) SELECT `linecategories`.* FROM `linecategories` WHERE `linecategories`.`id` = 6 LIMIT 1
(0.3ms) BEGIN
Linecategory Exists (0.4ms) SELECT 1 AS one FROM `linecategories` WHERE (`linecategories`.`slug` = BINARY 'our_dreams' AND `linecategories`.`id` != 6) LIMIT 1
SQL (0.2ms) UPDATE `linecategories` SET `sort` = 1, `updated_at` = '2015-07-20 21:07:47' WHERE `linecategories`.`id` = 6
(2.3ms) COMMIT
Linecategory Load (0.5ms) SELECT `linecategories`.* FROM `linecategories` WHERE `linecategories`.`id` = 4 LIMIT 1
(0.3ms) BEGIN
Linecategory Exists (0.7ms) SELECT 1 AS one FROM `linecategories` WHERE (`linecategories`.`slug` = BINARY 'beauty_and_health' AND `linecategories`.`id` != 4) LIMIT 1
SQL (0.3ms) UPDATE `linecategories` SET `sort` = 2, `updated_at` = '2015-07-20 21:07:47' WHERE `linecategories`.`id` = 4
(1.0ms) COMMIT
Linecategory Load (0.2ms) SELECT `linecategories`.* FROM `linecategories` WHERE `linecategories`.`id` = 1 LIMIT 1
(0.2ms) BEGIN
Linecategory Exists (0.7ms) SELECT 1 AS one FROM `linecategories` WHERE (`linecategories`.`slug` = BINARY 'horoscopes' AND `linecategories`.`id` != 1) LIMIT 1
SQL (0.2ms) UPDATE `linecategories` SET `sort` = 3, `updated_at` = '2015-07-20 21:07:47' WHERE `linecategories`.`id` = 1
(0.9ms) COMMIT
Linecategory Load (0.4ms) SELECT `linecategories`.* FROM `linecategories` WHERE `linecategories`.`id` = 2 LIMIT 1
(0.3ms) BEGIN
Linecategory Exists (0.4ms) SELECT 1 AS one FROM `linecategories` WHERE (`linecategories`.`slug` = BINARY 'test_yourself' AND `linecategories`.`id` != 2) LIMIT 1
SQL (0.5ms) UPDATE `linecategories` SET `sort` = 4, `updated_at` = '2015-07-20 21:07:48' WHERE `linecategories`.`id` = 2
(0.9ms) COMMIT
Linecategory Load (0.3ms) SELECT `linecategories`.* FROM `linecategories` WHERE `linecategories`.`id` = 5 LIMIT 1
(0.2ms) BEGIN
Linecategory Exists (0.5ms) SELECT 1 AS one FROM `linecategories` WHERE (`linecategories`.`slug` = BINARY 'divination' AND `linecategories`.`id` != 5) LIMIT 1
SQL (0.2ms) UPDATE `linecategories` SET `sort` = 5, `updated_at` = '2015-07-20 21:07:48' WHERE `linecategories`.`id` = 5
(0.8ms) COMMIT
Linecategory Load (0.3ms) SELECT `linecategories`.* FROM `linecategories` WHERE `linecategories`.`id` = 7 LIMIT 1
(0.1ms) BEGIN
Linecategory Exists (0.4ms) SELECT 1 AS one FROM `linecategories` WHERE (`linecategories`.`slug` = BINARY 'what_is_it_in_my_name' AND `linecategories`.`id` != 7) LIMIT 1
SQL (0.2ms) UPDATE `linecategories` SET `sort` = 6, `updated_at` = '2015-07-20 21:07:48' WHERE `linecategories`.`id` = 7
(0.9ms) COMMIT
Linecategory Load (0.3ms) SELECT `linecategories`.* FROM `linecategories` WHERE `linecategories`.`id` = 3 LIMIT 1
(0.2ms) BEGIN
Linecategory Exists (0.4ms) SELECT 1 AS one FROM `linecategories` WHERE (`linecategories`.`slug` = BINARY 'delicious' AND `linecategories`.`id` != 3) LIMIT 1
SQL (0.3ms) UPDATE `linecategories` SET `sort` = 7, `updated_at` = '2015-07-20 21:07:48' WHERE `linecategories`.`id` = 3
(1.0ms) COMMIT
I found better method
# /admin_categories PUT
def updates
data=params[:data].as_json(only: [:id, :sort])
db = ActiveRecord::Base.connection()
ids=[]
query = "UPDATE linecategories SET sort = CASE id "
data.each_with_index do |e, i|
query += ActiveRecord::Base.send(:sanitize_sql_array,["WHEN ? THEN ? ", e['id'], e['sort']])
ids.push e['id']
end
query += "END "
query += "WHERE id in (#{ids.join(",")}) "
db.execute(query)
render json: {}
end
I have two ActiveRecord queries. One returns 4 which is the right number, and the other returns 11, which is the wrong count. I would have expected both queries to return 4. Even the SQL rendered in Rails console are identical for both AR queries.
First query:
campaign.daily_statistics.select('COUNT(DISTINCT user_id) AS count').where(metric: metric).where("properties -> '#{column}' = '#{value}'")[0]['count']
DailyStatistic Load (0.7ms) SELECT COUNT(DISTINCT user_id) AS count FROM "daily_statistics" WHERE "daily_statistics"."campaign_id" = $1 AND "daily_statistics"."metric" = 'participation' AND (properties -> 'assumed_gender' = 'female') [["campaign_id", 2]]
=> 4
Second query:
sql = campaign.daily_statistics.select('COUNT(DISTINCT user_id) AS count')
sql.where(metric: metric).where("properties -> '#{column}' = '#{value}'")
sql[0]['count']
DailyStatistic Load (0.9ms) SELECT COUNT(DISTINCT user_id) AS count FROM "daily_statistics" WHERE "daily_statistics"."campaign_id" = $1 AND "daily_statistics"."metric" = 'participation' AND (properties -> 'assumed_gender' = 'female') [["campaign_id", 2]]
=> 11
Can someone explain what is going on here?
In your second query, you're not assigning
sql.where(metric: metric).where("properties -> '#{column}' = '#{value}'")
to anything. So when you then run
sql[0]['count']
you're only executing the first part of the query that you assigned to the sql variable.
I'm not sure why the SQL logging output appears as it does. At a guess, it's due to some of the peculiarities of the rails console and when it provides output/inline logging.
What you should be seeing in the rails console is something like:
sql = campaign.daily_statistics.select('COUNT(DISTINCT user_id) AS count')
DailyStatistic Load (0.9ms) SELECT COUNT(DISTINCT user_id) AS count FROM "daily_statistics" WHERE "daily_statistics"."campaign_id" = $1) [["campaign_id", 2]]
sql.where(metric: metric).where("properties -> '#{column}' = '#{value}'")
DailyStatistic Load (0.9ms) SELECT COUNT(DISTINCT user_id) AS count FROM "daily_statistics" WHERE "daily_statistics"."campaign_id" = $1 AND "daily_statistics"."metric" = 'participation' AND (properties -> 'assumed_gender' = 'female') [["campaign_id", 2]]
sql[0]['count']
#=> 11
a fix for the 2nd query should be would be:
sql = campaign.daily_statistics.select('COUNT(DISTINCT user_id) AS count')
DailyStatistic Load (0.9ms) SELECT COUNT(DISTINCT user_id) AS count FROM "daily_statistics" WHERE "daily_statistics"."campaign_id" = $1) [["campaign_id", 2]]
sql2 = sql.where(metric: metric).where("properties -> '#{column}' = '#{value}'")
DailyStatistic Load (0.9ms) SELECT COUNT(DISTINCT user_id) AS count FROM "daily_statistics" WHERE "daily_statistics"."campaign_id" = $1 AND "daily_statistics"."metric" = 'participation' AND (properties -> 'assumed_gender' = 'female') [["campaign_id", 2]]
sql2[0]['count']
#=> 4
A 'page' may have_many 'sections'. So I want to retrieve the 'sections.title' for each 'sections' that a page contains. I don't understand why my block Do doesn't work but this works sections.each { |n| p n.id }
2.0.0-p353 :041 > pages = Page.find(52).sections.count
Page Load (0.3ms) SELECT `pages`.* FROM `pages` WHERE `pages`.`id` = 52 LIMIT 1
(0.3ms) SELECT COUNT(*) FROM `sections` WHERE `sections`.`page_id` = 52
=> 2
2.0.0-p353 :042 > pages =Page.find(52).sections
Page Load (0.3ms) SELECT `pages`.* FROM `pages` WHERE `pages`.`id` = 52 LIMIT 1
Section Load (0.2ms) SELECT `sections`.* FROM `sections` WHERE `sections`.`page_id` = 52
=> #<ActiveRecord::Associations::CollectionProxy [#<Section id: 20, title: "La victoire est proche", body: "Guinsly t'e le meilleur Oat cake sweet roll browni...", page_id: 52, created_at: "2014-01-22 01:40:14", updated_at: "2014-01-22 01:40:14">, #<Section id: 36, title: "La victoire est proche", body: "Guinsly t'e le meilleur Oat cake sweet roll browni...", page_id: 52, created_at: "2014-01-22 01:40:15", updated_at: "2014-01-22 01:40:15">]>
2.0.0-p353 :044 > pages.each do |n|
2.0.0-p353 :045 > p.title
2.0.0-p353 :046?> end
NoMethodError: undefined method `title' for nil:NilClass
or
...
2.0.0-p353 :055 > Page.find(52).sections.each { |n| p n.id }
Page Load (0.3ms) SELECT `pages`.* FROM `pages` WHERE `pages`.`id` = 52 LIMIT 1
Section Load (0.3ms) SELECT `sections`.* FROM `sections` WHERE `sections`.`page_id` = 52
20
36
Your each block is looking for you to use n not p...
pages.each do |n|
n.title
end
Though p makes more sense, you might use p instead of n in both places.
Or are you trying to print the value using the p method? Then:
pages.each do |page|
p page.title
end
I have two models, projects and words where project has_many :words (words is really just a model that holds the quantity of words written each day for each project.
I have a view that I build like this, which shows all the days from start to end in the project and how many, if any words were written on that day:
<% project_range(#project.start, #project.end).each do |day| %>
<%= day %>
<%= get_word_count_by_date(#project, day ) %>
<% end %>
And in my helper:
def project_range(start, finish)
project_days = (start..finish).collect
end
def get_word_count_by_date(project, date)
word_count = Word.find_by_project_id_and_wrote_on(project, date)
if word_count
word_count.quantity
else
0
end
end
Trouble is, in the view, that hits my database a lot. For example, if the project is 30 days, I get:
Word Load (0.2ms) SELECT "words".* FROM "words" WHERE "words"."project_id" = 2 AND "words"."wrote_on" = '2011-09-01' LIMIT 1
Word Load (0.1ms) SELECT "words".* FROM "words" WHERE "words"."project_id" = 2 AND "words"."wrote_on" = '2011-09-02' LIMIT 1
Word Load (0.1ms) SELECT "words".* FROM "words" WHERE "words"."project_id" = 2 AND "words"."wrote_on" = '2011-09-03' LIMIT 1
Word Load (0.1ms) SELECT "words".* FROM "words" WHERE "words"."project_id" = 2 AND "words"."wrote_on" = '2011-09-04' LIMIT 1
Word Load (0.1ms) SELECT "words".* FROM "words" WHERE "words"."project_id" = 2 AND "words"."wrote_on" = '2011-09-05' LIMIT 1
Word Load (0.1ms) SELECT "words".* FROM "words" WHERE "words"."project_id" = 2 AND "words"."wrote_on" = '2011-09-06' LIMIT 1
Word Load (0.1ms) SELECT "words".* FROM "words" WHERE "words"."project_id" = 2 AND "words"."wrote_on" = '2011-09-07' LIMIT 1
Word Load (0.1ms) SELECT "words".* FROM "words" WHERE "words"."project_id" = 2 AND "words"."wrote_on" = '2011-09-08' LIMIT 1
Word Load (0.1ms) SELECT "words".* FROM "words" WHERE "words"."project_id" = 2 AND "words"."wrote_on" = '2011-09-09' LIMIT 1
Word Load (0.1ms) SELECT "words".* FROM "words" WHERE "words"."project_id" = 2 AND "words"."wrote_on" = '2011-09-10' LIMIT 1
Word Load (0.1ms) SELECT "words".* FROM "words" WHERE "words"."project_id" = 2 AND "words"."wrote_on" = '2011-09-11' LIMIT 1
Word Load (0.1ms) SELECT "words".* FROM "words" WHERE "words"."project_id" = 2 AND "words"."wrote_on" = '2011-09-12' LIMIT 1
Word Load (0.1ms) SELECT "words".* FROM "words" WHERE "words"."project_id" = 2 AND "words"."wrote_on" = '2011-09-13' LIMIT 1
Word Load (0.1ms) SELECT "words".* FROM "words" WHERE "words"."project_id" = 2 AND "words"."wrote_on" = '2011-09-14' LIMIT 1
Word Load (0.1ms) SELECT "words".* FROM "words" WHERE "words"."project_id" = 2 AND "words"."wrote_on" = '2011-09-15' LIMIT 1
Word Load (0.1ms) SELECT "words".* FROM "words" WHERE "words"."project_id" = 2 AND "words"."wrote_on" = '2011-09-16' LIMIT 1
Word Load (0.1ms) SELECT "words".* FROM "words" WHERE "words"."project_id" = 2 AND "words"."wrote_on" = '2011-09-17' LIMIT 1
Word Load (0.1ms) SELECT "words".* FROM "words" WHERE "words"."project_id" = 2 AND "words"."wrote_on" = '2011-09-18' LIMIT 1
Word Load (0.1ms) SELECT "words".* FROM "words" WHERE "words"."project_id" = 2 AND "words"."wrote_on" = '2011-09-19' LIMIT 1
Word Load (0.1ms) SELECT "words".* FROM "words" WHERE "words"."project_id" = 2 AND "words"."wrote_on" = '2011-09-20' LIMIT 1
Word Load (0.1ms) SELECT "words".* FROM "words" WHERE "words"."project_id" = 2 AND "words"."wrote_on" = '2011-09-21' LIMIT 1
Word Load (0.1ms) SELECT "words".* FROM "words" WHERE "words"."project_id" = 2 AND "words"."wrote_on" = '2011-09-22' LIMIT 1
Word Load (0.1ms) SELECT "words".* FROM "words" WHERE "words"."project_id" = 2 AND "words"."wrote_on" = '2011-09-23' LIMIT 1
Word Load (0.1ms) SELECT "words".* FROM "words" WHERE "words"."project_id" = 2 AND "words"."wrote_on" = '2011-09-24' LIMIT 1
Word Load (0.1ms) SELECT "words".* FROM "words" WHERE "words"."project_id" = 2 AND "words"."wrote_on" = '2011-09-25' LIMIT 1
Word Load (0.1ms) SELECT "words".* FROM "words" WHERE "words"."project_id" = 2 AND "words"."wrote_on" = '2011-09-26' LIMIT 1
Word Load (0.1ms) SELECT "words".* FROM "words" WHERE "words"."project_id" = 2 AND "words"."wrote_on" = '2011-09-27' LIMIT 1
Word Load (0.1ms) SELECT "words".* FROM "words" WHERE "words"."project_id" = 2 AND "words"."wrote_on" = '2011-09-28' LIMIT 1
Word Load (0.1ms) SELECT "words".* FROM "words" WHERE "words"."project_id" = 2 AND "words"."wrote_on" = '2011-09-29' LIMIT 1
Word Load (0.1ms) SELECT "words".* FROM "words" WHERE "words"."project_id" = 2 AND "words"."wrote_on" = '2011-09-30' LIMIT 1
Is there a way to do this without querying every single day in the length of the project? I tried starting by loading all of a project's words first, but couldn't figure out how to get the days with zero in there.
This is a "n+1" problem... What you want to do is join words and projects in your query so that all the words for each project are included in the result set.
Assuming that your project "has_many :words":
#project = Project.find(:id, :include => :words)
Now the words collection on each project will be pre-populated with the words in just 1 query.
Read more under the "Eager Loading of Associations" http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html
You could use a block helper to keep it clean and avoid looking them up:
def project_range(project, start, finish, &blk)
words = project.words.where(:wrote_on => start..finish)
word_map = words.index_by(&:wrote_on)
for day in start..finish
word_count = word_map[day] ? word_map[day].quantity : 0
blk.call(day, word_count)
end
end
Then use it like
<% project_range(project, start, finish) do |day, word_count| %>
<%= day %>
<%= word_count %>
<% end %>
You could also clean up the helper a bit (avoid having SQL in it), maybe by passing the list of pre-fetched words or using a scope
EDIT: m_x suggested the start..finish where clause on wrote_on which is cleaner!
I'd go for something like:
#words = #project.words.where("wrote_on >= ? and wrote_on <= ?", start, end)
and than use group_by to display them in the view:
#words.group_by(&:wrote_on).each do |day, word|
<%= day %>
<%= word.quantity %>
end