Scope Passing arguments in Rails - ruby-on-rails

In the Rails Active Record model we use scopes, for scopes, we pass the arguments. But my doubt is when we pass the default value to the argument it shows the result from that value, when we pass value when executing it shows the newly passed values result, how the values are over written
This is the scope in the model
scope :ftr_post,->(start_date = Date.yesterday,end_date = Date.tomorrow) {where("created_at between ? and ?",start_date,end_date)}
When I run the following in console
Post.ftr_post
Post Load (0.1ms) SELECT "posts".* FROM "posts" WHERE (created_at between '2023-01-24' and '2023-01-26')
It return default arguments
When I run the following
Post.ftr_post("2023-01-11","2023-01-20").count
Post Count (0.1ms) SELECT COUNT(*) FROM "posts" WHERE (created_at between '2023-01-11' and '2023-01-20')
it show the new passed arguments values
Please explain me in detail

You have written code for that and it is working fine.
scope :ftr_post,->(start_date = Date.yesterday,end_date = Date.tomorrow) {where("created_at between ? and ?",start_date,end_date)}
In the ruby, we use PARAMETER = ANY_VALUE in the method for defining default values.
Post.ftr_post
In the above first queries, you are not passing any argument hence it is taking default values start_date and end_date which are Date.yesterday and Date.tomorrow.
Post.ftr_post("2023-01-11","2023-01-20").count
In the second query, you are explicitly passing the arguments hence method will take that values instead of default values. Therefore, it is taking start_date as 2023-01-11 and end_date as 2023-01-20.
Please read here if you want to know more about parameter with default values in ruby

Related

Executing a SQL query with an `IN` clause from Rails code

I know precious nothing abour Rails, so please excuse my naivete about this question.
I'm trying to modify a piece of code that I got from somewhere to make it execute it for a randomly selected bunch of users. Here it goes:
users = RedshiftRecord.connection.execute(<<~SQL
select distinct user_id
from tablename
order by random()
limit 1000
SQL
).to_a
sql = 'select user_id, count(*) from tablename where user_id in (?) group by user_id'
<Library>.on_replica(:something) do
Something::SomethingElse.
connection.
exec_query(sql, users.join(',')).to_h
end
This gives me the following error:
ActiveRecord::StatementInvalid: PG::SyntaxError: ERROR: syntax error at or near ")"
LINE 1: ...ount(*) from tablename where user_id in (?) group by...
^
Users is an array, I know this coz I executed the following and it resulted in true:
p users.instance_of? Array
Would someone please help me execute this code? I want to execute a simple SQL query that would look like this:
select user_id, count(*) from tablename where user_id in (user1,user2,...,user1000) group by user_id
The problem here is that IN takes a list of parameters. Using a single bind IN (?) and a comma separated string will not magically turn it into a list of arguments. Thats just not how SQL works.
What you want is:
where user_id in (?, ?, ?, ...)
Where the number of binds matches the length of the array you want to pass.
The simple but hacky way to do this would be just interpolate in n number of question marks into the SQL string:
binds = Array.new(users.length, '?').join(',')
sql = <<~SQL
select user_id, count(*)
from tablename
where user_id in (#{binds)})
group by user_id'
SQL
<Library>.on_replica(:something) do
Something::SomethingElse.
connection.
exec_query(sql, users).to_h
end
But you would typically do this in a Rails app by creating a model and using the ActiveRecord query interface or using Arel to programatically create the SQL query.

Unique Random Record in Ruby

I have a Question table in which there is id# and title as columns.
Now, I need to randomly select 5 questions from the table. I am seeing people are using:
Question.order("RANDOM()").limit(5) //using postgre
Till now I have:
def selectr
#randquestion=[]
while #randquestion.length<3 do
Question.uncached do
ques=Question.order("RANDOM()").first
#randquestion << ques
end
end
end
I found uncaching from Ruby on Rails Active Record RANDOM() always the same within a loop.
But I am not sure if this will give me unique questions. I want 3 unique questions only.
You can do this by fetching each question via a separate SQL request, with subsequent requests excluding IDs for records already seen. Here's an example, tested with Rails 5.2 and MySQL:
def random_questions(number)
already_seen = []
number.times.map do
question = Question.order('RAND()').where.not(id: already_seen).first
already_seen << question.id
question
end
end
If you try this out with random_questions(3), you'll see:
Question Load (1.5ms) SELECT `questions`.* FROM `questions` WHERE 1=1 ORDER BY RAND() LIMIT 1
Question Load (1.4ms) SELECT `questions`.* FROM `questions` WHERE `questions`.`id` != 2 ORDER BY RAND() LIMIT 1
Question Load (1.3ms) SELECT `questions`.* FROM `questions` WHERE `questions`.`id` NOT IN (2, 1) ORDER BY RAND() LIMIT 1
As an aside, please note that order('RAND()') triggers a deprecation warning in newer versions of Rails:
DEPRECATION WARNING: Dangerous query method (method whose arguments are used as raw SQL) called with non-attribute argument(s): "RAND()". Non-attribute arguments will be disallowed in Rails 6.0. This method should not be called with user-provided values, such as request parameters or model attributes. Known-safe values can be passed by wrapping them in Arel.sql().
To avoid this warning, use .order(Arel.sql('RAND()')) instead.

Rails: Omit order by clause when using ActiveRecord .first query?

I'm having a problem with a .first query in Rails 4 ActiveRecord. New behavior in Rails 4 is to add an order by the id field so that all db systems will output the same order.
So this...
Foo.where(bar: baz).first
Will give the query...
select foos.* from foos order by foos.id asc limit 1
The problem I am having is my select contains two sum fields. With the order by id thrown in the query automatically, I'm getting an error that the id field must appear in the group by clause. The error is right, no need for the id field if I want the output to be the sum of these two fields.
Here is an example that is not working...
baz = Foo.find(77).fooviews.select("sum(number_of_foos) as total_number_of_foos, sum(number_of_bars) as total_number_of_bars").reorder('').first
Here is the error...
ActiveRecord::StatementInvalid: PG::GroupingError: ERROR: column "foos.id" must appear in the GROUP BY clause or be used in an aggregate function
LINE 1: ...bars FROM "fooviews" ORDER BY "...
Since the select is an aggregate expression, there is no need for the order by id, but AR is throwing it in automatically.
I found that I can add a reorder('') on to the end before the .first and that removes the order by id, but is that the right way to fix this?
Thank you
[UPDATE] What I neglected to mention is that I'm converting a large Rails 3 project to Rails 4. So the output from the Rails 3 is an AR object. If possible, the I would like the solution to keep in that format so that there is less code to change in the conversion.
You will want to use take:
The take method retrieves a record without any implicit ordering.
For example:
baz = Foo.find(77).fooviews.select("sum(number_of_foos) as total_number_of_foos, sum(number_of_bars) as total_number_of_bars").take
The commit message here indicates that this was a replacement for the old first behavior.

Retrieve database records in Rails

My model named Person contains 3 columns namely name,age,gender.
Now how to get all the rows if the gender = "male". I try to
fetch the data as shown below.
p = Person.find_by_gender("male")
The above statement properly worked. But it returns only 1 record. Because, the statement is converted to like following query.
SELECT "persons".* FROM "persons" WHERE "persons"."gender" = $1 LIMIT 1 [["gender", "male"]]
Due to limit is set to 1 it returns only 1 record. So, how to unset the limit? My requirement to get all the records in table if gender
matches "male".
use where
Person.where(gender: "male")
find method always returns only one record
In rails find methods always return single record that's why it returning single record.
Person.find_by_gender("male")
Use Where which give you array of matching records(which is ActiveRecord::Relation)
Person.where(:gender => "male")

Convert PSQL query to work using Rails 4 Objects

I have the following query:
select * from email_contacts
where extract(month from created_at) = 4
and call_type = 'Membership NPS'
and email NOT IN
(
select invite_email from nps_responses
where extract(month from fs_created_at) = 4
and invite_email is not null
)
Then I have a rails 4 app that has two corresponding models, EmailContact and NpsResponse.
How can I convert this query to run within my controller method in Rails? I need to select all email_contacts with the above criteria.
This might not be the perfect "Rails way" solution, but you could to do it by using find_by_sql. Using find_by_sql will still retrieve instantiated objects for your model object:
EmailContact.find_by_sql(your_sql_statement)
Use Bind Variables
To prevent SQL injection, you should use bind variables with the find_by_sql method (bind variables are placeholder parameters represented by the ? character. The find_by_sql method will bind the parameter values before executing the statement.) This is highly recommended from a security perspective.
Here's an example of using your SQL statement with bind variables in the find_by_sql method:
EmailContact.find_by_sql("select * from email_contacts
where extract(month from created_at) = ?
and call_type = ?
and email NOT IN
(
select invite_email from nps_responses
where extract(month from fs_created_at) = ?
and invite_email is not null
)", 4, 'Membership NPS', 4)
Things to notice:
The first parameter to the find_by_sql method is the SQL statement as a string value. In this particular SQL string, there are three ? characters as the placeholder variables.
The next three parameters in find_by_sql are the values to be bound to the ? character in the SQL statement.
The order of the parameters are important - the first bind variable parameter will bind to the first ? character, etc.
You can have a variable amount of bind variable parameters, as long as the number of ? matches the number of bind variable parameters.
For some better code organization, you can have some sort of helper method to construct the SQL statement with the bind variables. Kind of like this:
def email_contacts_SQL
sql = "select * from email_contacts
where extract(month from created_at) = ?
and call_type = ?
and email NOT IN
(
select invite_email from nps_responses
where extract(month from fs_created_at) = ?
and invite_email is not null
)"
end
And then use find_by_sql with your helper method:
EmailContact.find_by_sql(email_contacts_SQL, 4, 'Membership NPS', 4)
The official Rails API doc has more examples of how to use find_by_sql.
Warning: If you use find_by_sql, you lose database agnostic conversions provided by ActiveRecord. The find_by_sql method executes the SQL statement as is.
Something close to what you would write in ActiveRecord, this uses bind variables and protects against sql injection
EmailContact.where("month from created_at ?", some_value).
where(:call_type => 'Membership NPS').
where("email NOT IN (
select invite_email from nps_responses
where extract(month from fs_created_at) = ?
and invite_email is not null
)", some_value).all

Resources