What is the best way to implement update counter in Ecto/Phoenix? - ruby-on-rails

In ruby on rails(active record) we can update the counter just by calling
update_counter method. for example in rails you just need to write:
# For the Post with id of 5, decrement the comment_count by 1, and
# increment the action_count by 1
Post.update_counters 5, :comment_count => -1, :action_count => 1
# Executes the following SQL:
# UPDATE posts
# SET comment_count = comment_count - 1,
# action_count = action_count + 1
# WHERE id = 5
Is there any easy way to do this in elixir/phoenix considering I have to update the counter of multiple columns?

Alternativaly you can go with something like this using the inc option:
Post
|> where(id: 5)
|> Repo.update_all(inc: [comment_count: -1, action_count: 1])

I wrote this article almost a year ago now, but it's still relevant, and as a bonus, it's written from the perspective of "coming from Rails".
https://medium.com/#lukerollans_17079/counter-caching-in-phoenix-8ac372e5c0c5
Your best bet is to use prepare_changes/2 which provides a mechanism to run an arbitrary function in the same transaction. This allows you to do stuff like counter caching, or whatever you like.
It's also computed at the same time as generating your changeset, so you don't need to "specially" update counters at an arbitrary point in your codebase.

Related

Group by decade with counts

I have a table of Album's that has a date column named release_date.
I want to get a list of all the decades along with the number of albums released in that decade.
So, the output might be something like:
2010 - 11
2000 - 4
1990 - 19
1940 - 2
Ruby 2.3.1 w/ Rails 5 on Postgres 9.6, FWIW.
This is essentially a followup question to a previous one I had: Group by month+year with counts
Which may help with the solution...I'm just not sure how to do the grouping by decade.
Using Ruby for processing db data is inefficient in all senses.
I would suggest doing it on the database level:
Album.group("(DATE_PART('year', release_date)::int / 10) * 10").count
What happens here, is basically you take a year part of the release_date, cast it to integer, take it's decade and count albums for this group.
Say, we have a release_date of "2016-11-13T08:30:03+02:00":
2016 / 10 * 10
#=> 2010
Yes, this is pretty similar to your earlier question. In this case, instead of creating month/year combinations and using the combinations as your grouping criteria, you need a method that returns the decade base year from the album year.
Since you have a pattern developing, think about writing the code so it can be reused.
def album_decades
Album.all.map { |album| album.release_date.year / 10 * 10 }
end
def count_each(array)
array.each_with_object(Hash.new(0)) { |element, counts| counts[element] += 1 }
end
Now you can call count_each(album_decades) for the result you want. See if you can write a method album_months_and_years that will produce the result you want from your earlier question by calling count_each(album_months_and_years).
There are more than one possible solution to your problem, but I would try:
Add a new column to the Album table, called decade. You can use a migration for this porpoise.
Create a callback (its like a trigger, but in the programmer side) that set the decade value before saving the Album in the DB.
Finally you can use this useful query to group the Albums by decade. In your case would be Album.group(:decade).count wich would give you a hash with the numbers of Albums by decade.
...
Profit ?
Jokes aside, the callback should be something like:
class Album < ActiveRecord::Base
# some code ...
before_save :set_decade # this is the 'callback'
# ...
private
def set_decade
self.decade = self.release_date.year / 10
end
Then, if you use the step 3, it would return something like:
# => { '195' => 7, '200' => 12 }
I did not test the answer, so try it out and tell me how it went.

Rails, sum the aggregate of an attribute of all instances of a model

I have a model Channel. The relating table has several column, for example clicks.
So Channel.all.sum(:clicks) gives me the sum of clicks of all channels.
In my model I have added a new method
def test
123 #this is just an example
end
So now, Channel.first.test returns 123
What I want to do is something like Channel.all.sum(:test) which sums the test value of all channels.
The error I get is that test is not a column, which of course it is not, but I hoped to till be able to build this sum.
How could I achieve this?
You could try:
Channel.all.map(&:test).sum
Where clicks is a column of the model's table, use:
Channel.sum(:clicks)
To solve your issue, you can do
Channel.all.sum(&:test)
But it would be better to try achieving it on the database layer, because processing with Ruby might be heavy for memory and efficiency.
EDIT
If you want to sum by a method which takes arguments:
Channel.all.sum { |channel| channel.test(start_date, end_date) }
What you are talking about here is two very different things:
ActiveRecord::Calculations.sum sums the values of a column in the database:
SELECT SUM("table_name"."column_name") FROM "column_name"
This is what happens if you call Channel.sum(:column_name).
ActiveSupport also extends the Enumerable module with a .sum method:
module Enumerable
def sum(identity = nil, &block)
if block_given?
map(&block).sum(identity)
else
sum = identity ? inject(identity, :+) : inject(:+)
sum || identity || 0
end
end
end
This loops though all the values in memory and adds them together.
Channel.all.sum(&:test)
Is equivalent to:
Channel.all.inject(0) { |sum, c| sum + c.test }
Using the later can lead to serious performance issues as it pulls all the data out of the database.
Alternatively you do this.
Channel.all.inject(0) {|sum,x| sum + x.test }
You can changed the 0 to whatever value you want the sum to start off at.

Update multiple records in one ActiveRecord transaction in Rails

How can I update/save multiple instances of a model in one shot, using a transaction block in Rails?
I would like to update values for hundreds of records; the values are different for each record. This is not a mass-update situation for one attribute. Model.update_all(attr: value) is not appropriate here.
MyModel.transaction do
things_to_update.each do |thing|
thing.score = rand(100) + rand(100)
thing.save
end
end
save seems to issue it's own transaction, rather than batching the updates into the surrounding transaction. I want all the updates to go in one big transaction.
How can I accomplish this?
Say you knew that you wanted to set the things with ids 1, 2, and 3 to have scores, 2, 8, and 64 (as opposed to just random numbers), you could:
UPDATE
things AS t
SET
score = c.score
FROM
(values
(1, 2),
(2, 30),
(4, 50)
) as c(id, score)
where c.id = t.id;
So with Rails, you'd use ActiveRecord::Base.connection#execute to execute a block similar to the above, but with the correct value string interpolated.
I'm not sure, but possibly you are confusing multiple transactions with multiple queries.
The code you've posted will create a single transaction (e.g. if an exception occurred then all of the updates would be rolled back), but each save would result in a separate update query.
If it's possible to perform the update using SQL rather than Ruby code then that would probably be the best way to go.
I think that you need just change method "save" to "save!".
If any of the updates fail, the method save! generates an exception. When an exception occurs within a transaction block, the transaction reverses the entire operation (rollback)
MyModel.transaction do
things_to_update.each do |thing|
thing.score = rand(100) + rand(100)
thing.save!
end

Why is Resque incrementing a column count by 4-5 and not by 10?

Foobar.find(1).votes_count returns 0.
In rails console, I am doing:
10.times { Resque.enqueue(AddCountToFoobar, 1) }
My resque worker:
class AddCountToFoobar
#queue = :low
def self.perform(id)
foobar = Foobar.find(id)
foobar.update_attributes(:count => foobar.votes_count +1)
end
end
I would expect Foobar.find(1).votes_count to be 10, but instead it returns 4. If I run 10.times { Resque.enqueue(AddCountToFoobar, 1) } again, it returns the same behaviour. It only increments votes_count by 4 and sometimes 5.
Can anyone explain this?
This is a classic race condition scenario. Imagine that only 2 workers exist and that they each run one of your vote incrementing jobs. Imagine the following sequence.
Worker1: load foobar(vote count == 1)
Worker2: load foobar(vote count == 1, in a separate ruby object)
Worker 1: increment vote count (now == 2) and save
Worker 2: increment it's copy of foobar (vote count now == 2) and save, overwriting what worker 1 did
Although 2 workers ran 1 update job each, the count only increased by 1 because they were both operating on their own copy of foobar that wasn't aware of the change the other worker was doing
To solve this, you could either do an inplace style update, ie
UPDATE foos SET count = count + 1
or use one of the 2 forms of locking active record supports (pessimistic locking & optimistic locking)
The former works because the database ensures that you don't have concurrent updates on the same row at the same time.
Looks like ActiveRecord is not thread-safe in Resque (or rather redis, I guess). Here's a nice explanation.
As Frederick says, you're observing a race condition. You need to serialize access to the critical section from the time you read the value and update it.
I'd try to use pessimistic locking:
http://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html
http://api.rubyonrails.org/classes/ActiveRecord/Locking/Pessimistic.html
foobar = Foobar.find(id)
foobar.with_lock do
foobar.update_attributes(:count => foobar.votes_count +1)
end

Subquery in Rails report generation

I'm building a report in a Ruby on Rails application and I'm struggling to understand how to use a subquery.
Each 'Survey' has_many 'SurveyResponses' and it is simple enough to retrieve these however I need to group them according to one of the fields, 'jobcode', as I only want to report the information relating to a single jobcode in one line in the report.
However I also need to know the constituent data that makes up the totals for that jobcode. The reason for this is that I need to calculate data such as medians and standard deviations and so need to know the values that make the total.
My thinking is that I retrieve the distinct jobcodes that were reported on for the survey and then as I loop through these I retrieve the individual responses for each jobcode.
Is this the correct way to do this or should I follow a different method?
You could use a named scope to simplify getting the groups of responses:
named_scope :job_group, lambda{|job_code| {:conditions => ["job_code = ?", job_code]}}
Put that in your response model, aand use it like this:
job.responses.job_group('some job code')
and you'll get an array of responses. If you're looking to get the mean of the values of one of the attributes on the responses, you can use map:
r = job.responses.job_group('some job code')
r.map(&:total)
=> [1, 5, 3, 8]
Alternatively, you might find it quicker to write custom SQL in order to get the mean / average / sum of groups of attributes. Going through rails for this sort of work may cause significant lag.
ActiveRecord::Base.connection.execute("Custom SQL here")
You can also use Model.find_by_sql()
For example:
class User < Activerecord::Base
# Your usual AR model
end
...
def index
#users = User.find_by_sql "select * from users"
# etc
end

Resources