Random ActiveRecord with where and excluding certain records - ruby-on-rails

I would like to write a class function for my model that returns one random record that meets my condition and excludes some records. The idea is that I will make a "random articles section."
I would like my function to look like this
Article.randomArticle([1, 5, 10]) # array of article ids to exclude
Some pseudo code:
ids_to_exclude = [1,2,3]
loop do
returned_article = Article.where(published: true).sample
break unless ids_to_exclude.include?(returned_article.id)
do

Lets look at DB specific option.
class Article
# ...
def self.random(limit: 10)
scope = Article.where(published: true)
# postgres, sqlite
scope.limit(limit).order('RANDOM()')
# mysql
scope.limit(limit).order('RAND()')
end
end
Article.random asks the database to get 10 random records for us.
So lets look at how we would add an option to exclude some records:
class Article
# ...
def self.random(limit: 10, except: nil)
scope = Article.where(published: true)
if except
scope = scope.where.not(id: except)
end
scope.limit(limit).order('RANDOM()')
end
end
Now Article.random(except: [1,2,3]) would get 10 records where the id is not [1,2,3].
This is because .where in rails returns a scope which is chain-able. For example:
> User.where(email: 'test#example.com').where.not(id: 1)
User Load (0.7ms) SELECT "users".* FROM "users" WHERE "users"."email" = $1 AND ("users"."id" != $2) [["email", "test#example.com"], ["id", 1]]
=> #<ActiveRecord::Relation []>
We could even pass a scope here:
# cause everyone hates Bob
Article.random( except: Article.where(author: 'Bob') )
See Rails Quick Tips - Random Records for why a DB specific solution is a good choice here.

You can use some like this:
ids_to_exclude = [1,2,3,4]
Article.where("published = ? AND id NOT IN (?)", true , ids_to_exclude ).order( "RANDOM()" ).first

Related

How to create Rails query that (I think) requires PostgreSQL subquery?

Not sure how to do this so title may not be correct.
Each User has a field country of type String.
Given an array of user_id, country tuples for the query, find all the records that match. Each User must be found with it's own country.
For example, here is the array of tuples.
[1, 'us'],
[2, 'mexico'],
[3, 'us']
This would return User 1 if it exists and its country is 'us'.
It should also return User 2 if it exists and its country is 'mexico'.
The query should return all matching results.
Rails 4.2
class User < ApplicationRecord
def self.query_from_tuples(array_of_tuples)
array_of_tuples.inject(nil) do |scope, (id, country)|
if scope
scope.or(where(id: id, country: country))
else
where(id: id, country: country) # this handles the initial iteration
end
end
end
end
The resulting query is:
SELECT "users".* FROM "users"
WHERE (("users"."id" = $1 AND "users"."country" = $2 OR "users"."id" = $3 AND "users"."country" = $4) OR "users"."id" = $5 AND "users"."country" = $6)
LIMIT $7
You could also adapt kamakazis WHERE (columns) IN (values) query by:
class User < ApplicationRecord
def self.query_from_tuples_2(array_of_tuples)
# just a string of (?,?) SQL placeholders for the tuple values
placeholders = Array.new(array_of_tuples.length, '(?,?)').join(',')
# * is the splat operator and turns the tuples (flattened) into
# a list of arguments used to fill the placeholders
self.where("(id, country) IN (#{placeholders})", *array_of_tuples.flatten)
end
end
Which results in the following query which is a lot less verbose:
SELECT "users".* FROM "users"
WHERE ((id, country) IN ((1,'us'),(2,'mexico'),(3,'us'))) LIMIT $1
And can also perform much better if you have a compound index on [id, country].
I know this would work in pure SQL: e.g.
SELECT * FROM user
WHERE (id, country) IN ((1, 'us'), (2, 'mexico'), (3, 'us'))
Now I don't know how Rails would handle the bind parameter if it was a list of pairs (list of two elements each). Perhaps that would work.
You can construct a raw sql and use active record. Something like this:
def self.for_multiple_lp(arr=[])
# Handle case when arr is not in the expected format.
condition = arr.collect{|a| "(user_id = #{a[0]} AND country = #{a[1]})"}.join(" OR ")
where(condition)
end
Edit: Improved Solution
def self.for_multiple_lp(arr=[])
# Handle case when arr is not in the expected format.
condition = arr.collect{|a| "(user_id = ? AND country = ?)"}.join(" OR ")
where(condition, *(arr.flatten))
end
This should work.

Optimize ActiveRecord queries. Is it possible to combine two queries into one?

Optimize ActiveRecord queries. Is it possible to combine two queries into one?
I need get all records, and first record I use.
#appointments ||= #lead.appointments.order(created_at: :desc)
if appt = #appointments.first
json.latest_appointment do
json.partial! 'api/v1/lead_appointments/appointment', appointment: appt
end
end
json.appointments do
json.array! #appointments do |a|
json.partial! 'api/v1/lead_appointments/appointment', appointment: a
end
end
and get similar sql query
Appointment Load (0.4ms) SELECT "appointments".* FROM "appointments" WHERE "appointments"."lead_id" = $1 ORDER BY "appointments"."created_at" DESC LIMIT $2 [["lead_id", 760730], ["LIMIT", 1]]`
Appointment Load (0.4ms) SELECT "appointments".* FROM "appointments" WHERE "appointments"."lead_id" = $1 ORDER BY "appointments"."appointment_at" DESC [["lead_id", 760730]]`
I'd change your if to
if appt = #appointments.to_a.first
This will cause your appointments to be loaded to an array with one query without limit, the first will work on an array and not cause an additional query to the DB and then when you render them all, they're already fetched from the DB.
Alternatively, your if can also be
if appt = #appointments[0]

Rails Data Modelling: How to establish relationships when 2 Objects "Share" the same has_many list?

In a nutshell, when I create a Transaction Record it has two foreign keys. for the two users that participate in a Transaction, ie:
Now, What I would like to know with your kind help, is How do I establish the relationship(s) between: User and Transaction Models, so that I can easily retrieve ALL the Transactions for either of the two Users.
Something like:
user_one = User.find(1)
user_two = User.find(2)
user_one.transactions # returns all Transactions where user_one.id ==
# user_one_id Or user_one.id == user_two_id
user_two.transactions # returns all Transactions where user_two.id ==
# user_one_id Or user_two.id == user_two_id
What's the best way to achieve this? Is it best to establish foreign keys in the Transaction Model in this case? Or is this a problem to be solved via ActiveRecordQuery only?
Thanks in advance.
If you have two user ids and want to query Transaction on some combination of them, you can use an or clause:
>> Transaction.where(user_one_id: 1).or(Transaction.where(user_two_id: 2))
Transaction Load (4.3ms) SELECT "transactions".* FROM "transactions" WHERE ("transactions"."user_one_id" = $1 OR "transactions"."user_two_id" = $2) LIMIT $3 [["user_one_id", 1], ["user_two_id", 2], ["LIMIT", 11]]
=> #<ActiveRecord::Relation [#<Transaction id: 1, user_one_id: 1, user_two_id: 2, created_at: "2017-09-12 23:25:39", updated_at: "2017-09-12 23:25:39">]>
this is sample code
class Transaction
def move
return ""
end
end
class User1 < Transaction
def move
return 'User1 move: X'
end
end
class User2 < Transaction
def move
return'User2 move: O'
end
end
transactions = [User1.new, User2.new]
transactions.each {|tran|
print tran.move
}
Use polymorfic assossiations that can solve this issue I am in the train now will provide you with code or you can start looking up for this task and good luck ;)

How to query by ActiveRecord depending If the value of an attribute is empty or not?

I have a variable that could be set or left empty. In the first case the query looks fine, but in the second one I can't find how it works.
Checking If :
if params['customer_id'] == ""
#customer_id = "";
else
#customer_id = params['customer_id']
end
The query
User.where("customer_id = ?", #customer_id)
The problem is that If "" ,the query returns nothing. I could write it as
if params['customer_id'] == ""
User.all
else
User.where("customer_id = ?", params['customer_id'])
end
but first this is not DRY and second my query will include 10 * where's so this is not a very smart way to accomplish it.
You will refactor your query as:
#users = User.all
#users = #users.where(customer_id: params['customer_id']) if params['customer_id'].present?
Example:
#users = User.all
# User Load (6.8ms) SELECT "users".* FROM "users" WHERE "users"."deleted_at" IS NULL
#users.where(email: 'arup').count
# (1.2ms) SELECT COUNT(*) FROM "users" WHERE "users"."deleted_at" IS NULL AND "users"."email" = $1 [["email", "arup"]]
update
with scope
scope :with_or_without_customer, ->(customer_id) do
customer_id.present? ? where(customer_id: customer_id) : all
end
Note:
Model.all now returns an ActiveRecord::Relation, rather than an array of records. Use Relation#to_a if you really want an array.
In some specific cases, this may cause breakage when upgrading. However in most cases the ActiveRecord::Relation will just act as a lazy-loaded array and there will be no problems.
You can try this way.
#condition = "1"
#condition = {:customer_id => params['customer_id']} if params['customer_id'].present?
You can create your condition first then fire the query on database so that it will fire query only single time on database:
#users = User.where(#condition)

Compare associations on different ActiveRecords without fetching from the DB

I would like to be able to compare associated records on ActiveRecords, without actually fetching from the database. The following will do that comparison, but hits the DB when I make the comparison
employee1 = Employee.find_by(name: 'Alice')
DEBUG Employee Load (92.0ms) SELECT "employees".* FROM "employees" WHERE "employees"."name" = 'Alice' LIMIT 1
employee2 = Employee.find_by(name: 'Bob')
DEBUG Employee Load (92.0ms) SELECT "employees".* FROM "employees" WHERE "employees"."name" = 'Bob' LIMIT 1
employee1.manager == employee2.manager
DEBUG Employee Load (697.9ms) SELECT "employees".* FROM "employees" WHERE "employees"."id" = $1 ORDER BY "employees"."id" ASC LIMIT 1 [["id", 53]]
DEBUG Employee Load (504.1ms) SELECT "employees".* FROM "employees" WHERE "employees"."id" = $1 ORDER BY "employees"."id" ASC LIMIT 1 [["id", 53]]
=> true
I can compare the values of the foreign columns directly, but that's less idiomatic and can be difficult to refactor later on:
employee1.manager_id == employee2.manager_id
=> true
EDIT: I've added my own answer as a solution to this question below
If you know you're going to be needing/using the Manager for the Employee during the operation, you can make sure you load that object when the employee is loaded, that will prevent the trip back to the database:
employee1 = Employee.includes(:manager).find_by(name: 'Alice')
employee2 = Employee.includes(:manager).find_by(name: 'Bob')
employee1.manager == employee2.manager
=> true # database hit not needed...
That or just compare the IDs, but make a helper method on Employee like
class Employee
def same_manager?(other_employee)
other_employee.manager_id == self.manager_id
end
end
At least that way it's given a name and the operation within it makes sense in context.
I'm going to post my own answer for the time being. I've monkey-patched Active Record to include a new method, compare_association, which allows you to compare foreign objects on different ActiveRecords without hitting the DB.
module ActiveRecord
class Base
def compare_association(association_name, record)
association = self.class.reflect_on_association(association_name)
foreign_key = association.foreign_key
return self.read_attribute(foreign_key) == record.read_attribute(foreign_key)
end
end
end
Example:
# Compare the 'manager' association of `employee1` and `employee2`
# Equivalent to `employee1.manager_id == employee2.manager_id` but without
# referencing the DB columns by name.
employee1.compare_association(:manager, employee2)

Resources