Rails - Join tables to sort posts - ruby-on-rails

In my rails app I have a table Designs which are essentially the posts. Users can like these Designs (using the thumbs_up gem). This table is called Votes. On user's profile pages, I'm showing all of the Designs that they have liked (or voted for). In the user model, I have a method:
def favorites
Design.joins(:votes).where('voter_id = ?', self.id).where('voteable_type = ?', 'Design').where('vote = ?', true)
end
Then in the user's controller to call these designs I have
def show
#designs = #user.favorites
end
This shows all of the designs they have liked, but it's in order of when the Design was created, not when the Vote was created. The Vote table has a created_at column so I know I can sort these Designs based on when they liked them.
I tried this with no luck
def favorites
results = Design.joins(:votes).where('voter_id = ?', self.id).where('voteable_type = ?', 'Design').where('vote = ?', true)
results.sort! {|t1, t2| t2.vote.created_at <=> t1.vote.created_at}
end
How can I sort the Designs based on when that user liked them.
the Vote table has these columns
vote: boolean
voteable_id: integer
voteable_type: string
voter_id: integer
created_at: date
updated_at: date
thanks!

The prior answer doesn't work because scopes work at the Class level, so when you use it on a instance of User, it assumes favorites is a instance method or association.
What I recommend you do is use the Vote model, and reference the belongs_to Design association on Vote:
def show
#votes = #user.votes.includes(:design).order(created_at: :asc)
#designs = #votes.collect {|v| v.design }
end
You could feel free to move that to the User model in place of your favorites method like so:
def favorites
#favorites ||= self.votes.includes(:design).order(created_at: :asc).collect {|v| v.design }
end
UPDATE
Since you are using this thumbs_up gem, the following will work:
In Action
def show
#designs = #user.votes.where(voteable_type: 'Design').order(created_at: :asc).collect {|v| v.voteable}
end
Or Method
def favorites
#favorites ||= self.votes.where(voteable_type: 'Design').order(created_at: :asc).collect {|v| v.voteable}
end
https://github.com/bouchard/thumbs_up

I think you'll be able to use a scope for this - I'd use a scope on the user model to communicate with the votes model from your user instance:
#app/models/user.rb
Class User < ActiveRecord::Base
has_many :votes, as :voteable
has_many :designs, through: :votes
scope :favorites, -> { joins(:votes).where('voter_id = ? AND votable_type = ? AND vote = ?', id, 'Design', true) }
scope :latest, -> { order(created_at: :asc) }
scope :latest_favorites, -> { favorites.order(created_at: :asc) }
end
This will allow you to call:
#user.favorites.latest
#user.latest_favorites

Related

How to make scope, that will output custom data?

I have models Category and Transactions.
Category has_many transactions, Transaction belongs_to category.
And i have scope for Category:
#relation = Category.all
#relation.joins(:transactions).where('transactions.created_at >= ?', 1.month.ago).
group('categories.id').order('SUM(transactions.debit_amount_cents) DESC')
It displays categories and sorts them by sum of transactions.debit_amount_cents
I want to display the amount for all its transactions along with each category.
Like:
id: 1,
name: "Category1",
all_amount: *some value* #like this
How can I improve this scope?
class Category < ApplicationRecord
# remember that scope is just a widely abused syntactic sugar
# for writing class methods
def self.with_recent_transactions
joins(:transactions)
.where('transactions.created_at >= ?', 1.month.ago)
.select(
'categories.*',
'SUM(transactions.debit_amount_cents) AS total_amount'
)
.order('total_amount DESC')
.group('categories.id')
end
end
If you select a column or an aggregate and give it an alias it will be available on the resulting model instances.
Category.with_recent_transactions.each do |category|
puts "#{category.name}: #{category.total_amount}"
end
For portability you can write this with Arel instead of SQL strings which avoids hardcoding stuff like table names:
class Category < ApplicationRecord
def self.with_recent_transactions
t = Transaction.arel_table
joins(:transactions)
.where(transactions: { created_at: Float::Infinity..1.month.ago })
.select(
arel_table[Arel.star]
t[:debit_amount_cents].sum.as('total_amount')
)
.order(total_amount: :desc) # use .order(t[:debit_amount_cents].sum) on Oracle
.group(:id) # categories.id on most adapters except TinyTDS
end
end
In Rails 6.1 (backported to 6.0x) you can use beginless ranges to create GTE conditions without Float::Infinity:
.where(transactions: { created_at: ..1.month.ago })

Rails Join based on multiple models

I have four models in question: User, Product, Purchase, and WatchedProduct.
I am trying to get a list of products that meet one of the following criteria:
I created the product.
I bought the product.
The product is free, and I have "starred" or "watched" it.
This is what I have so far:
class User < ActiveRecord::Base
...
def special_products
product_ids = []
# products I created
my_products = Product.where(:user_id => self.id).pluck(:id)
# products I purchased
my_purchases = Purchase.where(:buyer_id => self.id).pluck(:product_id)
# free products I watched
my_watched = WatchedProduct.where(:user_id =>self.id).joins(:product).where(products: { price: 0 }).pluck(:product_id)
product_ids.append(my_products)
product_ids.append(my_purchases)
product_ids.append(my_watched)
product_ids # yields something like this => [[1, 2], [], [2]]
# i guess at this point i'd reduce the ids, then look them up again...
product_ids.flatten!
product_ids & product_ids
products = []
product_ids.each do |id|
products.append(Product.find(id))
end
products
end
end
What I am trying to do is get a list of Product models, not a list of IDs or a list of ActiveRecord Relations. I am very new to joins, but is there a way to do all of this in a single join instead of 3 queries, reduce, and re lookup?
First I like adding few scopes
class Product < ActiveRecord::Base
scope :free, -> { where(price: 0) }
scope :bought_or_created_by, lambda do |user_id|
where('user_id = :id OR buyer_id = :id', id: user_id)
end
end
class WatchedProduct < ActiveRecord::Base
scope :by_user, ->(user_id) { where(user_id: user_id) }
end
Then the queries
special_products = (Product.bought_or_created_by(id) + Product.joins(:watched_products).free.merge(WatchedProduct.by_user(id)).uniq
This will return an array of unique products using 2 queries.
Although i am not sure about your model associations but yes you can do all these things in single query somehow like this:
Product.joins([{:user => :watched_products}, :buyer, :purchases]).where(["users.id = :current_buyer && buyers.id = :current_buyer && watched_products.user_id = :current_buyer && purchases.buyer_id = :current_buyer, products.price = 0", :current_buyer => self.id])
I am assuming
Product belongs_to user and buyer
Product has_many purchases
User has_many watched_products

Filter for values OR IS NULL in activeadmin

Given I have two models:
class Post < ActiveRecord::Base
belongs_to :district
end
class District < ActiveRecord::Base
has_many :posts
end
I need to make a check_boxes filter in ActiveAdmin on Posts page for with ability for user to get posts that belong to some exact districts or does not belong to any districts at all.
Before ActiveAdmin changed MetaSearch to Ransack, that could be done with custom scope. And now I don't have any idea. When I do the following:
filter :district_id_null, as: :boolean
filter :district, as: :check_boxes
It makes condition WHERE district_id IN (1,2,3) AND district_id IS NULL (I need OR instead of AND). And when I do
filter :district, as: :check_boxes, collection: proc { District.unscoped.map { |x| [x.title, x.id] }.unshift ['Empty', 'null'] }
It makes condition WHERE district_id IN (0,1,2,3) (but in most SQL databases NULL is not 0).
I think something like this might work
class Post
def self.ransack_with_or(search_params)
params = search_params.deep_clone
#check if we need OR
if search_params.has_key?('district_id_in') && search_params.has_key?('district_id_null')
params.delete('district_id_null')
district_id_in = params.delete('district_id_in')
#old behaviour without district_id_null and district_id_null attributes
q = ransack_without_or(params)
#here we're adding OR group
q.build_grouping({m: 'or', district_id_null: true, district_id_in: district_id_in})
else
#old behaviour we don't need OR
q = ransack_without_or(params)
end
q
end
#wrapping ransack method
class << self
alias_method_chain :ransack, :or
end
end

Using Delta Indexes for associations in Thinking Sphinx

I have a Product model:
class Product < ActiveRecord::Base
belongs_to :subcategory
define_index do
# fields
indexes subcategory.name, :as => :subcategory, :sortable => true, :facet => true
# attributes
has subcategory_id, created_at, updated_at
#properties
set_property :delta => true
Now, suppose that a user updates a subcategory name, which is the proper way to update the products delta index?
According to this documentation: http://freelancing-god.github.com/ts/en/deltas.html, a save message should be sent to the product, so in this case I should go for each product related with the subcategory and send the save message, something like this:
class Subcategory < ActiveRecord::Base
has_many :products
after_save :set_product_delta_flag
private
def set_product_delta_flag
products.each { |product|
product.delta = true
product.save
}
end
end
I think that this is overkilling because we have like 100.000 products per subcategory.
Is this the correct way to update the delta index? Am I missing something?
After adding this:
def set_product_delta_flag
Product.update_all ['delta = ?', true], ['subcategory_id = ?', id]
Product.index_delta
end
I'm always receiving this error:
NoMethodError (undefined method `index_delta' for #):
So, the solution to this problem was to send the message *define_indexes* to the Product model.
After fixing this issue, everything was ok, but the delta_index was not correctly updated, I needed to do save twice to the subcategory model.
So my final solution is this one:
after_commit :set_product_delta_flag
private
def set_product_delta_flag
Product.define_indexes
Product.update_all ['delta = ?', true], ['subcategory_id = ?', id]
Product.index_delta
end
Using after_commit and define_indexes is the correct solution? Its the only one that I've found.
Try the following instead:
def set_product_delta_flag
Product.update_all ['delta = ?', true], ['subcategory_id = ?', id]
Product.index_delta
end
A single SQL statement, a single delta re-indexing. Should perform far better :)

Rails ActiveRecord - Best way to perform an include?

I have three models:
class Book < ActiveRecord::Base
has_many :collections
has_many :users, :through => :collections
end
class User < ActiveRecord::Base
has_many :collections
has_many :books, :through => :collections
end
class Collection < ActiveRecord::Base
belongs_to :book
belongs_to :user
end
I'm trying to display a list of the books and have a link to either add or remove from the user's collection. I can't quite figure out the best syntax to do this.
For example, if I do the following:
Controller
class BooksController < ApplicationController
def index
#books = Book.all
end
end
View
...
<% if book.users.include?(current_user) %>
...
or obviously the inverse...
...
<% if current_user.books.include?(book) %>
...
Then queries are sent for each book to check on that include? which is wasteful. I was thinking of adding the users or collections to the :include on the Book.all, but I'm not sure this is the best way. Effectively all I need is the book object and just a boolean column of whether or not the current user has the book in their collection, but I'm not sure how to forumlate the query in order to do that.
Thanks in advance for your help.
-Damien
I have created a gem(select_extra_columns) for returning join/calculated/aggregate columns in a ActiveRecord finders. Using this gem, you will be able to get the book details and the flag indicating if the current user has the book in one query.
In your User model register the select_extra_columns feature.
class Book < ActiveRecord::Base
select_extra_columns
has_many :collections
has_many :users, :through => :collections
end
Now in your controller add this line:
#books = Book.all(
:select => "books.*, IF(collections.id, 1, 0) AS belongs_to_user",
:extra_columns => {:belongs_to_user => :boolean},
:joins => "LEFT OUTER JOIN collections
ON book.id = collections.book_id AND
collections.user_id = #{current_user.id}"
)
Now in your view you can do the following.
book.belongs_to_user?
You're going to to want 2 SQL queries, and O(1) based lookups (probably irrelevant, but it's the principle) to check if they have the book.
The initial calls.
#books = Book.all
#user = User.find(params[:id], :include => :collections)
Next, you're going to want to write the books the user has into a hash for constant time lookup (if people won't ever have many books, just doing an array.include? is fine).
#user_has_books = Hash.new
#user.collections.each{|c|#user_has_books[c.book_id] = true}
And on the display end:
#books.each do |book|
has_book = #user_has_books.has_key?(book.id)
end
I'd err away from caching the book_ids on the user object, simply because going this route can have some funny and unexpected consequences if you ever start serializing your user objects for whatever reason (i.e. memcached or a queue).
Edit: Loading intermediary collection instead of double loading books.
Essentially you need to make one call to get the book information and the Boolean flag indicating if the current user has the book. ActiveRecord finders doesn't allow you to return the join results from another table. We work around this problem by doing a trick.
In your Book model add this method.
def self.extended_book
self.columns # load the column definition
#extended_user ||= self.clone.tap do |klass|
klass.columns << (klass.columns_hash["belongs_to_user"] =
ActiveRecord::ConnectionAdapters::Column.new(
"belongs_to_user", false, "boolean"))
end # add a dummy column to the cloned class
end
In your controller use the following code:
#books = Book.extended_book.all(
:select => "books.*, IF(collections.id, 1, 0) AS belongs_to_user",
:joins => "LEFT OUTER JOIN collections
ON book.id = collections.book_id AND
collections.user_id = #{current_user.id}"
)
Now in your view you can do the following.
book.belongs_to_user?
Explanation:
In the extended_book method you are creating a copy of Book class and adding a dummy column belongs_to_user to the hash. During the query extra join column is not rejected as it exists in the columns_hash. You should use the extended_book only for querying.
If you use it for CRUD operations DB will throw error.
I would first create an instance method in the User model that 'caches' the all the Book ID's in his collection:
def book_ids
#book_ids ||= self.books.all(:select => "id").map(&:id)
end
This will only execute the SQL query once per controller request. Then create another instance method on the User model that takes a book_id as a parameter and checks to see if its included in his book collection.
def has_book?(book_id)
book_ids.include?(book_id)
end
Then while you iterate through the books:
<% if current_user.has_book?(book.id) %>
Only 2 SQL queries for that controller request :)
Use exists? on the association as it is direct SQL call. The association array is NOT loaded to perform these checks.
books.users.exists?(current_user)
This is the SQL executed by Rails.
SELECT `users`.id FROM `users`
INNER JOIN `collections` ON `users`.id = `collections`.user_id
WHERE (`users`.`id` = 2) AND ((`collections`.book_id = 1)) LIMIT 1
In the above SQL current_user id = 2 and book id is 1
current_user.books.exists?(book)
This is the SQL executed by Rails.
SELECT `books`.id FROM `books`
INNER JOIN `collections` ON `books`.id = `collections`.book_id
WHERE (`books`.`id` = 3) AND ((`collections`.user_id = 4)) LIMIT 1
In the above SQL current_user id = 4 and book id is 3
For more details, refer to the documentation of the exists? method in a :has_many association.
Edit: I have included additional information to validate my answer.

Resources