Rails 3 ActiveRecord where clause where id is set or null - ruby-on-rails

I have an ActiveRecord, Annotation, with two columns: user_id and post_id, which can be null. An annotation with null user_id and post_id are "global" annotations that are applicable to all posts across all users.
I would like to retrieve all annotations associated with a particular user's post AND all global annotations. In MySQL, the SQL command would be
select * from annotations where (user_id = 123 and post_id = 456) or (user_id is null and post_id is null)
In Rails 3, what is best way to code this as a Annotation.where clause?

Unfortunately there is no really great way to use OR sql syntax. Your best two options are probably to write the where clause yourself using bound parameters:
annotations = Annotation.where("(user_id = ? and post_id = ?) OR (user_id is null and post_id is null)").all
Or you can dive into Arel and use that syntax to craft it (check out asciicast's Arel post).
Warning, everything in the or call is psuedocode, no idea how to do 'post_id is null' - you may have to just pass "user_id is null and post_id is null" to or(), but my best guess is:
annotations = Annotation.arel_table
annotations.where(annotations[:user_id].eq(123).
and(annotations[:post_id].eq(456)).
or(annotations[:user_id].eq(nil).and(annotations[:post_id].eq(nil))

Related

say `Post` is a model, a class that inherits from `ApplicationRecord`, in rails. Then, what does Post.arel_table.create_table_alias does?

Lets say I have this code:
new_and_updated = Post.where(:published_at => nil).union(Post.where(:draft => true))
post = Post.arel_table
Post.from(post.create_table_alias(new_and_updated, :posts))
I have this code from a post about arel, but does not really explains what create_table_alias does. Only that at the end the result is an active activeRecord::Relation object, that is the result of the previously defined union. Why is needed to pass :posts, as a second param for create_table_alias, is this the name of the table in the database?
The Arel is essentially as follows
alias = Arel::Table.new(table_name)
table = Arel::Nodes::As.new(table_definition,alias)
This creates a SQL alias for the new table definition so that we can reference this in a query.
TL;DR
Lets explain how this works in terms of the code you posted.
new_and_updated= Post.where(:published_at => nil).union(Post.where(:draft => true))
This statement can be converted into the following SQL
SELECT
posts.*
FROM
posts
WHERE
posts.published_at IS NULL
UNION
SELECT
posts.*
FROM
posts
WHERE
posts.draft = 1
Well that is a great query but you cannot select from it as a subquery without a Syntax Error. This is where the alias comes in so this line (as explained above in terms of Arel)
post.create_table_alias(new_and_updated, :posts)
becomes
(SELECT
posts.*
FROM
posts
WHERE
posts.published_at IS NULL
UNION
SELECT
posts.*
FROM
posts
WHERE
posts.draft = 1) AS posts -- This is the alias
Now the wrapping Post.from can select from this sub-query such that the final query is
SELECT
posts.*
FROM
(SELECT
posts.*
FROM
posts
WHERE
posts.published_at IS NULL
UNION
SELECT
posts.*
FROM
posts
WHERE
posts.draft = 1) AS posts
BTW your query can be simplified a bit if you are using rails 5 and this removes the need for the rest of the code as well e.g.
Post.where(:published_at => nil).or(Post.where(:draft => true))
Will become
SELECT
posts.*
FROM
posts
WHERE
posts.published_at IS NULL OR posts.draft = 1
From the Rails official doc, from query method does this:
Specifies table from which the records will be fetched.
So, in order to fetch posts from the new_and_updated relation, we need to have an alias table which is what post.create_table_alias(new_and_updated, :posts) is doing.
Rubydoc for Arel's create_table_alias method tells us that the instance method is included in Table module.
Here :posts parameter is specifying the name of the alias table to create while new_and_updated provides ActiveRecord::Relation object.
Hope that helps.

Add computable column to multi-table select clause with eager_load in Ruby on Rails Activerecord

I have a query with a lot of joins and I'm eager_loading some of associations at the time. And I need to compute some value as attribute of one of models.
So, I'm trying this code:
ServiceObject
.joins([{service_days: :ou}, :address])
.eager_load(:address, :service_days)
.where(ous: {id: OU.where(sector_code: 5)})
.select('SDO_CONTAINS(ous.service_area_shape, SDO_GEOMETRY(2001, 8307, sdo_point_type(addresses.lat, addresses.lng, NULL), NULL, NULL) ) AS in_zone')
Where SQL function call in select operates data from associated addresses and ous tables.
I'm getting next SQL (so my in_zone column getting calculated and returned as first column before other columns for all eager_loaded models):
SELECT SDO_CONTAINS(ous.service_area_shape, SDO_GEOMETRY(2001, 8307, sdo_point_type(addresses.lat, addresses.lng, NULL), NULL, NULL) ) AS in_zone, "SERVICE_OBJECTS"."ID" AS t0_r0, "SERVICE_OBJECTS"."TYPE" AS t0_r1, <omitted for brevity> AS t2_r36 FROM "SERVICE_OBJECTS" INNER JOIN "SERVICE_DAYS" ON "SERVICE_DAYS"."SERVICE_OBJECT_ID" = "SERVICE_OBJECTS"."ID" INNER JOIN "OUS" ON "OUS"."ID" = "SERVICE_DAYS"."OU_ID" INNER JOIN "ADDRESSES" ON "ADDRESSES"."ID" = "SERVICE_OBJECTS"."ADDRESS_ID" WHERE "OUS"."ID" IN (SELECT "OUS"."ID" FROM "OUS" WHERE "OUS"."SECTOR_CODE" = :a1) [["sector_code", "5"]]
But it seems like that in_zone isn't accessible from either model used in query.
I need to have calculated in_zone as attribute of ServiceObject model object, how I can accomplish that?
Ruby on Rails 4.2.6, Ruby 2.3.0, oracle_enhanced adapter 1.6.7, Oracle 12.1
I have successfully replicated your issue and it turns out that this is a known issue in Rails. The problem is that when using eager_load, Rails maps the columns of all eager-loaded tables into table and column aliases in the form of t0_r0, t0_r1, etc... (you can see these in the SQL that you pasted in the question). And while doing that, it simply ignores the custom columns in the select, probably because it cannot determine which eager-loaded table it should attribute the custom column to. It is sad that this issue is open for more than 2 years now...
Nevertheless I think I found a workaround. It seems that if you don't eager load the tables but manually join them (with joins), you can as well include them (with includes) and the custom columns will be returned as there will be no column aliasing taking place. The point is that you must not use associations in the joins clauses but you have to specify the joins yourself. Also note that you must specify all columns from the main table in the select manually too (see the service_objects.* in the select).
Try the following approach:
ServiceObject
.joins('INNER JOIN "SERVICE_DAYS" ON "SERVICE_DAYS"."SERVICE_OBJECT_ID" = "SERVICE_OBJECTS"."ID"')
.joins('INNER JOIN "OUS" ON "OUS"."ID" = "SERVICE_DAYS"."OU_ID"')
.joins('INNER JOIN "ADDRESSES" ON "ADDRESSES"."ID" = "SERVICE_OBJECTS"."ADDRESS_ID"')
.includes(:service_days, :address)
.where(ous: {id: OU.where(sector_code: 5)})
.select('service_objects.*, SDO_CONTAINS(ous.service_area_shape, SDO_GEOMETRY(2001, 8307, sdo_point_type(addresses.lat, addresses.lng, NULL), NULL, NULL) ) AS in_zone')
The computation in the select should still work as the related tables are joined together but there should be no column aliasing present.
Of course this approach means that you'll get three queries instead of just one but unless you return a huge amount of records, the following two queries run by the includes clause should be very fast as they simply load the relevant records using foreign keys.
That monkey patch helped #Envek:
module ActiveRecord
Base.send :attr_accessor, :_row_
module Associations
class JoinDependency
JoinBase && class JoinPart
def instantiate_with_row(row, *args)
instantiate_without_row(row, *args).tap { |i| i._row_ = row }
end; alias_method_chain :instantiate, :row
end
end
end
end
then it is possible to do:
ServiceObject
.joins([{service_days: :ou}, :address])
.eager_load(:address, :service_days)
.where(ous: {id: OU.where(sector_code: 5)})
.select('SDO_CONTAINS(ous.service_area_shape, SDO_GEOMETRY(2001, 8307, sdo_point_type(addresses.lat, addresses.lng, NULL), NULL, NULL) ) AS in_zone')
.first
._row_['in_zone']

Combine multiple queries into one active record relation?

I am trying to write a search query for my app where based on the query string it will search for groups or users matching the string.
Here is what I have written:
def search_api
#groups = Group.where("name ILIKE '#{params[:query]}'")
#users = User.where("first_name ILIKE '#{params[:query]}' OR last_name ILIKE '#{params[:query]}")
end
Is there a way to combine these two queries into one activerecord relation array? Besides the brute force iterating over both arrays and putting them into one?
I am not sure about the solution but I have a security suggestion.
Your queries are not SQL Injection safe. You could pass array instead of injecting params in SQL string.
The following queries are SQL injection safe:
#groups = Group.where("name ILIKE ?", "#{params[:query]}")
#users = User.where("first_name ILIKE ? OR last_name ILIKE ?", "#{params[:query]}")
So here is a solution. Not sure that it is better than leaving everything as it is, but formally it's the answer to a question (besides that the result is not an ActiveRecord relation):
Group.connection.execute("(SELECT id, 'groups' as table FROM groups WHERE...) UNION (SELECT id, 'users' as table FROM users WHERE...)")
This returns an object of type PG::Result which you can treat as array of hashes. And, as it has already been said, it is good to pass arguments as an array instead of inserting them directly into SQL. Unfortunately, if You want to get a result as ActiveRecord, you may use UNION only for different queries to one table. In that case it looks like:
Group.find_by_sql("(SELECT * FROM groups WHERE...) UNION (SELECT * FROM groups WHERE...)")

ActiveRecord Rails: Get records using a condition in the relation

Here is an example: a User has many Cars and a Car belongs to a User.
I would like to extract all cars information, but not the cars of some users (this is because I would like to remove me and some colleagues to create correct stats).
I tried this, but without success:
Car.where("car.user.name != 'john'")
Any idea? Do you have a general rule about getting records with conditions in the relation?
Thanks.
Try the follwing:
Car.where('user_id != ?', User.find_by_<name?>('john').id
If you have rails 4:
Car.where.not(user_id: User.find_by(name: 'john')).id
UPDATE:
The solution above will work because you have a foreign key you can query against. More general solution is to perform left join with association table and filter those results. THe following will work regardless of association type (including has_many :through and has_and_belongs_to_many):
Car.includes(:user).where('users.name != ?', 'john')
# rails 4
Car.includes(:user).where.not(users: { name: 'john'})
Car.includes(:users).where("users.name NOT IN ('bill','peter','joe') OR cars.user_id IS NULL")
It will return all cars that don't have a user name that is Bill, Peter or Joe, and also cars that don't belong to a user !
Have a try with
Car.joins([:user]).where("users.name != 'John'")
If you need all the cars information with user information like users.name, then you can have
Car.joins([:user]).select('cars.*, users.name AS user_name').where("users.name != 'John'")
The above does the thing with a single sql query
SELECT cars.*, users.name AS user_name FROM `cars` INNER JOIN `users` ON `users`.`id` = `cars`.`user_id` WHERE (users.name != 'John')

Merge 2 relations on OR instead of AND

I have these two pieces of code that each return a relation inside the Micropost model.
scope :including_replies, lambda { |user| where("microposts.in_reply_to = ?", user.id)}
def self.from_users_followed_by(user)
followed_user_ids = user.followed_user_ids
where("user_id IN (?) OR user_id = ?", followed_user_ids, user)
end
When I run r1 = Micropost.including_replies(user) I get a relation with two results with the following SQL:
SELECT `microposts`.* FROM `microposts` WHERE (microposts.in_reply_to = 102) ORDER BY
microposts.created_at DESC
When I run r2 = Micropost.from_users_followed_by(user) I get a relation with one result with the following SQL:
SELECT `microposts`.* FROM `microposts` WHERE (user_id IN (NULL) OR user_id = 102) ORDER
BY microposts.created_at DESC
Now when I merge the relations like so r3 = r1.merge(r2) I got zero results but was expecting three. The reason for this is that the SQL looks like this:
SELECT `microposts`.* FROM `microposts` WHERE (microposts.in_reply_to = 102) AND
(user_id IN (NULL) OR user_id = 102) ORDER BY microposts.created_at DESC
Now what I need is (microposts.in_reply_to = 102) OR (user_id IN (NULL) OR user_id = 102)
I need an OR instead of an AND in the merged relation.
Is there a way to do this?
Not directly with Rails. Rails does not expose any way to merge ActiveRelation (scoped) objects with OR. The reason is that ActiveRelation may contain not only conditions (what is described in the WHERE clause), but also joins and other SQL clauses for which merging with OR is not well-defined.
You can do this either with Arel directly (which ActiveRelation is built on top of), or you can use Squeel, which exposes Arel functionality through a DSL (which may be more convenient). With Squeel, it is still relevant that ActiveRelations cannot be merged. However Squeel also provides Sifters, which represent conditions (without any other SQL clauses), which you can use. It would involve rewriting the scopes as sifters though.

Resources