ActiveRecord change or reset ordering defined in scope - ruby-on-rails

I have a function which uses another functions output: an ActiveRecord::Relation object. This relation already has an order clause:
# This function cannot be changed
def black_box
Product.where('...').order("name")
end
def my_func
black_box.order("id")
end
when I execute the relation the ORDER_BY clause is ordered by the order functions:
SELECT * FROM products
WHERE ...
ORDER_BY('name', 'id') // The first order function, then the second
Is there any way I can specify the relation to insert my order function BEFORE the previous one? So the SQL would look like so?
SELECT * FROM products
WHERE ...
ORDER_BY('id', 'name')

You could use reorder method to reset the original order and add your new order by column.
reorder(*args)
Replaces any existing order defined on the relation with the specified order.
User.order('email DESC').reorder('id ASC') # generated SQL has 'ORDER BY id ASC'
Subsequent calls to order on the same relation will be appended. For example:
User.order('email DESC').reorder('id ASC').order('name ASC')
# generates a query with 'ORDER BY id ASC, name ASC'.

Related

How sorting works in ruby on rails? when there are multiple fields to sort in a single query

I have a model with the fields price, min_price,max_price, discount,in my product table. if I want to execute ascending descending orders, how that will get executed when we apply for an order on multiple fields. for example like below.
#products = Product.order("price asc").order("min_price desc").order("max_price asc").order("updated_at asc") (Query might be wrong but for reference im adding)
will it order as per the order sequence ?
If you append .to_sql to that, it will show the generated SQL so you can investigate yourself.
I tried a similar query:
Book.select(:id).order("id asc").order("pub_date desc").to_sql
=> "SELECT \"books\".\"id\" FROM \"books\" ORDER BY id asc, pub_date desc"
You might instead:
Book.select(:id).order(id: :asc, pub_date: :desc).to_sql
=> "SELECT \"books\".\"id\" FROM \"books\" ORDER BY \"books\".\"id\" ASC, \"books\".\"pub_date\" DESC"
... which you see adds the table name in, so is more reliable when if you are accessing multiple tables

Rails remove duplicated associated records

Let's say I have a User and User has_many :tags and I would like to remove all #users tags that have duplicated name. For example,
#user.tags #=> [<Tag name: 'A'>, <Tag name: 'A'>, <Tag name: 'B'>]
I would like to keep only the tags with unique names and delete the rest from the database.
I know I could pull out a list of unique tags names from user's tags and remove all users's tags and re-create user's tags with only unique names but it would be ineffficient?
On the other hand, select won't work as it returns only the selected column. uniq also won't work:
#user.tags.uniq #=> returns all tags
Is there a more efficient way?
UPDATE:
I would like to do this in a migration.
This method will give you an ActiveRecord::Relation with the duplicate tags:
class Tag < ApplicationRecord
belongs_to :user
def self.duplicate_tags
unique = self.select('DISTINCT ON(tags.name, tags.user_id) tags.id')
.order(:name, :user_id, :id)
self.where.not(id: unique)
end
end
Its actually run as a single query:
SELECT "tags".* FROM "tags"
WHERE "tags"."id" NOT IN
(SELECT DISTINCT ON(tags.name) tags.id
FROM "tags" GROUP BY "tags"."id", "tags"."user_id"
ORDER BY tags.name, tags.id)
You can remove the duplicates in a single query with #delete_all.
# Warning! This can't be undone!
Tag.duplicate_tags.destroy_all
If you need to destroy dependent associations or call your before_* or after_destroy callbacks, use the #destroy_all method instead. But you should use this together with #in_batches to avoid running out of memory.
# Warning! This can't be undone!
Tag.duplicate_tags.in_batches do |batch|
# destroys a batch of 1000 records
batch.destroy_all
end
You can write SQL model-independent query in the migration.
Here is PostgreSQL-specific migration code:
execute <<-SQL
DELETE FROM tags
WHERE id NOT IN (
SELECT DISTINCT ON(user_id, name) id FROM tags
ORDER BY user_id, name, id ASC
)
SQL
And here is more SQL common one:
execute <<-SQL
DELETE FROM tags
WHERE id IN (
SELECT DISTINCT t2.id FROM tags t1
INNER JOIN tags t2
ON (
t1.user_id = t2.user_id AND
t1.name = t2.name AND
t1.id < t2.id
)
)
SQL
This SQL fiddle shows
different queries you can use as sub-select in DELETE query depending on your goals: deleting first/last/all duplicates.

ActiveRecord distinct doesn't work

I made a Select using Active Record with a lot of Joins. This resulted in duplicate values. After the select function there's the distinct function with value :id. But that didn't work!
Here's the code:
def join_query
<<-SQL
LEFT JOIN orders on orders.purchase_id = purchases.id
LEFT JOIN products on products.id = orders.complete_product_id
SQL
end
def select_query
<<-SQL
purchases.*,
products.reference_code as products_reference_code
SQL
end
result = Purchase.joins(join_query)
.select(select_query)
.distinct(:id)
Of course, neither distinct! or uniq functions worked. The distinct! returned a error from "ActiveRecord::ImmutableRelation" that I don't know what means.
To fix this I did a hack, converting the ActiveRecord_Relation object to an Array and I used the uniq function of Ruby.
What's going on here?
try this out:
def select_query
<<-SQL
DISTINCT ON (purchases.id) purchases.id,
products.reference_code as products_reference_code
SQL
end
add more comma separated column names in select clause
Purchase.select(select_query).joins(join_query)

Why does rails add "order_by id" to all queries? Which ends up breaking Postgres

The following rails code:
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
Generates the following query (note the order_by added by Rails):
SELECT
sum(up_votes) total_up_votes
FROM
"answers"
WHERE
"answers"."user_id" = 100
ORDER BY
"answers"."id" ASC
This throws an error in Postgres:
PG::GroupingError: ERROR: column "answers.id" must appear in the GROUP BY clause or be used in an aggregate function
Is rails' database abstraction only made with MySQL in mind?
No, the 'order by id' is added to ensure .first always returns the same result. Without an ORDER BY clause, the result is not guaranteed to be the same under the SQL spec.
For your case, you should use .sum() instead of .select() to do this more simply:
def top?
self.answers.sum(:up_votes) >= 10
end
You used #first method at the end. That's the reason.
self.answers.select("sum(up_votes) total_up_votes").first # <~~~
Model.first finds the first record ordered by the primary key
Look at the clause
ORDER BY
"answers"."id" ASC # this is the primary key of your table ansers.
Check the documentation of 1.1.3 first or #first .

Rails: How to sort many-to-many relation

I have a many-to-many relationship between a model User and Picture. These are linked by a join table called Picturization.
If I obtain a list of users of a single picture, i.e. picture.users -> how can I ensure that the result obtained is sorted by either creation of the Picturization row (i.e. the order at which a picture was associated to a user). How would this change if I wanted to obtain this in order of modification?
Thanks!
Edit
Maybe something like
picture.users.where(:order => "created_at")
but this created_at refers to the created_at in picturization
Have an additional column something like sequence in picturization table and define sort order as default scope in your Picturization
default_scope :order => 'sequence ASC'
If you want default sort order based on modified_at then use following default scope
default_scope :order => 'modified_at DESC'
You can specify the table name in the order method/clause:
picture.users.order("picturizations.created_at DESC")
Well, in my case, I need to sort many-to-many relation by a column named weight in the middle-table. After hours of trying, I figured out two solutions to sort many-to-many relation.
Solution1: In Rails Way
picture.users.where(:order => "created_at")
cannot return a ActiveRecord::Relation sorted by Picturization's created_at column.
I have tried to rewrite a default_scope method in Picturization, but it does not work:
def self.default_scope
return Picturization.all.order(weight: :desc)
end
Instead, first, you need to get the ids of sorted Picturization:
ids = Picturization.where(user_id:user.id).order(created_at: :desc).ids
Then, you can get the sorted objects by using MySQL field functin
picture.users.order("field(picturizations.id, #{ids.join(",")})")
which generates SQL looks like this:
SELECT `users`.*
FROM `pictures` INNER JOIN `picturizations`
ON `pictures`.`id` = `picturizations`.`picture_id`
WHERE `picturizations`.`user_id = 1#for instance
ORDER BY field(picturizations.id, 9,18,6,8,7)#for instance
Solution2: In raw SQL Way
you can get the answer directly by using an order by function:
SELECT `users`.*
FROM `pictures` INNER JOIN `picturizations`
ON `pictures`.`id` = `picturizations`.`picture_id`
WHERE `picturizations`.`user_id = 1
order by picturizations.created_at desc

Resources