Rails query executes 2 queries - mysql

I have a simple rails query like
a = A.where(type: 'user')
if a.count > 1
#Log Information
end
return a
Rails does lazy loading where it doesn't query the database unless some operation on the result set is executed. This is a fine behavior. But in my case rails ends up executing 2 queries because I call a.count before operating on a
SELECT COUNT(*) FROM `a` WHERE `a`.`type` = 'user';
SELECT `a`.* FROM `a` WHERE `a`.`type` = 'user';
Is there any way I can ask rails to perform the query immediately so that only the second query is executed and the count is returned from the dataset.

You can force the results into an array. I think that to_a will work for that, but entries is a clearer way express that intention since its job is to iterate over the items in an Enumerable and return an array of the enumerated results.
a = A.where(type: 'user').entries
if a.count > 1
#Log Information
end
return a

Related

How to use the distinct method in Rails with Arel Table?

I am looking to run the following query in Rails (I have used the scuttle.io site to convert my SQL to rails-friendly syntax):
Here is the original query:
SELECT pools.name AS "Pool Name", COUNT(DISTINCT stakings.user_id) AS "Total Number of Users Per Pool" from stakings
INNER JOIN pools ON stakings.pool_id = pools.id
INNER JOIN users ON users.id = stakings.user_id
INNER JOIN countries ON countries.code = users.country
WHERE countries.kyc_flow = 1
GROUP BY (pools.name);
And here is the scuttle.io query:
<%Staking.select(
[
Pool.arel_table[:name].as('Pool_Name'), Staking.arel_table[:user_id].count.as('Total_Number_of_Users_Per_Pool')
]
).where(Country.arel_table[:kyc_flow].eq(1)).joins(
Staking.arel_table.join(Pool.arel_table).on(
Staking.arel_table[:pool_id].eq(Pool.arel_table[:id])
).join_sources
).joins(
Staking.arel_table.join(User.arel_table).on(
User.arel_table[:id].eq(Staking.arel_table[:user_id])
).join_sources
).joins(
Staking.arel_table.join(Country.arel_table).on(
Country.arel_table[:code].eq(User.arel_table[:country])
).join_sources
).group(Pool.arel_table[:name]).each do |x|%>
<p><%=x.Pool_Name%><p>
<p><%=x.Total_Number_of_Users_Per_Pool%>
<%end%>
Now, as you may notice, sctuttle.io does not include the distinct parameter which I need. How in the world can I use distinct here without getting errors such as "method distinct does not exist for Arel Node?" or just syntax errors?
Is there any way to write the above query using rails ActiveRecord? I am sure there is, but I am really not sure how.
Answer
The Arel::Nodes::Count class (an Arel::Nodes::Function) accepts a boolean value for distinctness.
def initialize expr, distinct = false, aliaz = nil
super(expr, aliaz)
#distinct = distinct
end
The #count expression is a shortcut for the same and also accepts a single argument
def count distinct = false
Nodes::Count.new [self], distinct
end
So in your case you could use either of the below options
Arel::Nodes::Count.new([Staking.arel_table[:user_id]],true,'Total_Number_of_Users_Per_Pool')
# OR
Staking.arel_table[:user_id].count(true).as('Total_Number_of_Users_Per_Pool')
Suggestion 1:
The Arel you have seems a bit overkill. Given the natural relationships you should be able to simplify this a bit e.g.
country_table = Country.arel_table
Staking
.joins(:pools,:users)
.joins( Arel::Nodes::InnerJoin(
country_table,
country_table.create_on(country_table[:code].eq(User.arel_table[:country])))
.select(
Pool.arel_table[:name],
Staking.arel_table[:user_id].count(true).as('Total_Number_of_Users_Per_Pool')
)
.where(countries: {kyc_flow: 1})
.group(Pool.arel_table[:name])
Suggestion 2: Move this query to your controller. The view has no business making database calls.

Dynamically passing of parameter in rails sql query

Below is an SQL query which fetches some data related to user.
def self.get_user_details(user_id)
result = Event.execute_sql("select replace(substring_index(properties, 'text', -1),'}','') as p, count(*) as count
from ahoy_events e where e.user_id = ?
group by p order by count desc limit 5", :user_id)
return result
end
I want to dynamically pass values to user id to get the result.
I am using the below method to sanitize sql array, but still it returns no result. The query works fine if given static parameter.
def self.execute_sql(*sql_array)
connection.execute(send(:sanitize_sql_array, sql_array))
end
Because the query is complicated I am couldn't figure out the ActiveRecord way to get the results.
Is there any way I could get this sorted out?
I don't know why that should not work. May SQL-Syntac SELECT * FROM users ...
dynamic passing some other way you can do this: "some string #{user.id#ruby code} some more string"
You can do querys in Activerecord like this: User.where(id: user.id).where(field2: value2).group_by(:somevalue).order(:id)

Does the Laravel `increment()` lock the row?

Does calling the Laravel increment() on an Eloquent model lock the row?
For example:
$userPoints = UsersPoints::where('user_id','=',\Auth::id())->first();
if(isset($userPoints)) {
$userPoints->increment('points', 5);
}
If this is called from two different locations in a race condition, will the second call override the first increment and we still end up with only 5 points? Or will they add up and we end up with 10 points?
To answer this (helpful for future readers): the problem you are asking about depends on database configuration.
Most MySQL engines: MyISAM and InnoDB etc.. use locking when inserting, updating, or altering the table until this feature is explicitly turned off. (anyway this is the only correct and understandable implementation, for most cases)
So you can feel comfortable with what you got, because it will work correct at any number of concurrent calls:
-- this is something like what laravel query builder translates to
UPDATE users SET points += 5 WHERE user_id = 1
and calling this twice with starting value of zero will end up to 10
The answer is actually a tiny bit different for the specific case with ->increment() in Laravel:
If one would call $user->increment('credits', 1), the following query will be executed:
UPDATE `users`
SET `credits` = `credits` + 1
WHERE `id` = 2
This means that the query can be regarded as atomic, since the actual credits amount is retrieved in the query, and not retrieved using a separate SELECT.
So you can execute this query without running any DB::transaction() wrappers or lockForUpdate() calls because it will always increment it correctly.
To show what can go wrong, a BAD query would look like this:
# Assume this retrieves "5" as the amount of credits:
SELECT `credits` FROM `users` WHERE `id` = 2;
# Now, execute the UPDATE statement separately:
UPDATE `users`
SET `credits` = 5 + 1, `users`.`updated_at` = '2022-04-15 23:54:52'
WHERE `id` = 2;
Or in a Laravel equivalent (DONT DO THIS):
$user = User::find(2);
// $user->credits will be 5.
$user->update([
// Shown as "5 + 1" in the query above but it would be just "6" ofcourse.
'credits' => $user->credits + 1
]);
Now, THIS can go wrong easily since you are 'assigning' the credit value, which is dependent on the time that the SELECT statement took place. So 2 queries could update the credits to the same value while the intention was to increment it twice. However, you CAN correct this Laravel code the following way:
DB::transaction(function() {
$user = User::query()->lockForUpdate()->find(2);
$user->update([
'credits' => $user->credits + 1,
]);
});
Now, since the 2 queries are wrapped in a transaction and the user record with id 2 is READ-locked using lockForUpdate(), any second (or third or n-th) instance of this transaction that takes place in parallel should not be able to read using a SELECT query until the locking transaction is complete.

Need to convert an array to Integer for a MySQL query in a Rails API

I have a piece of code which fetches the list of ids of users I follow.
#followed = current_user.followed_user_ids
It gives me a result like this [11,3,24,42]
I need to add these to NOT IN mysql query.
Currently, I am getting NOT IN ([11,3,24,42]) which is throwing an error. I need NOT IN (11,3,24,42)
This is a part of a find_by_sql statement, so using where.not is not possible for me in this point.
In rails 4:
#followed = current_user.followed_user_ids # #followed = [11,3,24,42]
#not_followed = User.where.not(id: #followed)
This should generate something like select * from users where id not in (11,3,24,42)
As you comment, you are using find_by_slq (and that is available in all rails versions). Then you could use the join method:
query = "select * from users where id not in (#{#followed.join(',')})"
This would raise mysql errors if #followed is blank, the resulting query would be
select * from users where id not in ()
To solve this whiout specifiying aditional if statements to your code, you can use:
query = "select * from users where id not in (0#{#followed.join(',')})"
Your normal queries would be like:
select * from users where id not in (01,2,3,4)
but if the array is blank then would result in
select * from users where id not in (0)
which is a still valid sql statement and is delivering no results (which might be the expected situation in your scenario).
you can do something like:
#followed = [11,3,24,42]
User.where('id not in (?)', #followed)

MySQL to Postgres group_by error

The following query works fine in MySQL (which is generated by rails):
SELECT
sum(up_votes) total_up_votes
FROM
"answers"
WHERE
"answers"."user_id" = 100
ORDER BY
"answers"."id" ASC
Yet it gives the following error in Postgres:
PG::GroupingError: ERROR: column "answers.id" must appear in the GROUP BY clause or be used in an aggregate function
Edit: updated query. And here is the Rails model where this query is performed:
class User < MyModel
def top?
data = self.answers.select("sum(up_votes) total_up_votes").first
return (data.total_up_votes.present? && data.total_up_votes >= 10)
end
end
The query may execute in MySQL, but I doubt that it "works fine". It returns only one row, so the ordering is meaningless.
Your question is vague on what you actually want to do, but this should produce the same results as your query, in both databases:
SELECT sum(up_votes) as total_up_votes
FROM answers
WHERE user_id = 100;