Select the complement of a set - ruby-on-rails

I am using Rails 3.0. I have two tables: Listings and Offers. A Listing has-many Offers. An offer can have accepted be true or false.
I want to select every Listing that does not have an Offer with accepted being true. I tried
Listing.joins(:offers).where('offers.accepted' => false)
However, since a Listing can have many Offers, this selects every listing that has non-accepted Offers, even if there is an accepted Offer for that Listing.
In case that isn't clear, what I want is the complement of the set:
Listing.joins(:offers).where('offers.accepted' => true)
My current temporary solution is to grab all of them and then do a filter on the array, like so:
class Listing < ActiveRecord::Base
...
def self.open
Listing.all.find_all {|l| l.open? }
end
def open?
!offers.exists?(:accepted => true)
end
end
I would prefer if the solution ran the filtering on the database side.

The first thing that comes to mind is to do essentially the same thing you're doing now, but in the database.
scope :accepted, lambda {
joins(:offers).where('offers.accepted' => true)
}
scope :open, lambda {
# take your accepted scope, but just use it to get at the "accepted" ids
relation = accepted.select("listings.id")
# then use select values to get at those initial ids
ids = connection.select_values(relation.to_sql)
# exclude the "accepted" records, or return an unchanged scope if there are none
ids.empty? ? scoped : where(arel_table[:id].not_in(ids))
}
I'm sure this could be done more cleanly using an outer join and grouping, but it's not coming to me immediately :-)

Related

Using state attributes to maintain old records in Rails

I want to keep old records that would be normally destroyed. For example, an user joins a project, and is kicked from it later on. I want to keep the user_project record with something that flags the record as inactive. For this I use a state attribute in each model to define the current state of each record.
Almost all my "queries" want just the "active" records, the records with state == 1, and I want to use the ActiveRecord helpers (find_by etc). I don't want to add to all the "find_by's" I use a "_and_state" to find only the records that are active.
This is what I have now:
u = UserProject.find_by_user_id_and_project_id id1, id2
This is what I will have for every query like this for all models:
u = UserProject.find_by_user_id_and_project_id_and_state id1, id2, 1
What is the most cleaner way to implement this (the state maintenance and the cleaner query code)?
create a scope in your model UserProject:
class UserProject < ActiveRecord::Base
scope :active, where(:state => 1)
end
and "filter" your queries:
u = UserProject.active.find_by_user_id_and_project_id id1, id2
if you "almost allways" query the active UserProjects only, you can define this scope as default_scope and use unscoped if you want to query all records:
class UserProject < ActiveRecord::Base
default_scope where(:state => 1)
end
u = UserProject.find_by_user_id_and_project_id id1, id2 # only active UserProjects
u = UserProject.unscoped.find_by_user_id_and_project_id id1, id2 # all states
Here's a range of soft deletion gems you may want to choose from, which offer a nice abstraction that's already been thought through and debugged:
rails3_acts_as_paranoid
acts_as_archive
paranoia
Although if this happens to be your first Rails app, I second Martin's advice of rolling your own implementation.
I tried to just add this to Martin's answer, but my edit has to be reviewed, so even though Martin's answer was great, we can improve on it a little with the idea of default scopes. A default scope is always applied to finders on the model you add them to unless you specifically turn off the default scope:
class UserProject < ActiveRecord::Base
default_scope where(:state => 1)
end
The example Martin gave then becomes:
u = UserProject.find_by_user_id_and_project_id id1, id2
In this case, even without specifying that you want state == 1, you will only get active records. If this is almost always what you want, using a default scope will ensure you don't accidentally leave off the '.active' somewhere in your code, potentially creating a hard-to-find bug.
If you specify your default scope like this:
default_scope :conditions => {:state => 1}
then newly created UserProjects will already have state set to 1 without you having to explicitly set it.
Here's more information on default scopes: http://apidock.com/rails/ActiveRecord/Base/default_scope/class
Here's how to turn them off temporarily when you need to find all records:
http://apidock.com/rails/ActiveRecord/Scoping/Default/ClassMethods/unscoped

Use Ruby's select method on a Rails relation and update it

I have an ActiveRecord relation of a user's previous "votes"...
#previous_votes = current_user.votes
I need to filter these down to votes only on the current "challenge", so Ruby's select method seemed like the best way to do that...
#previous_votes = current_user.votes.select { |v| v.entry.challenge_id == Entry.find(params[:entry_id]).challenge_id }
But I also need to update the attributes of these records, and the select method turns my relation into an array which can't be updated or saved!
#previous_votes.update_all :ignore => false
# ...
# undefined method `update_all' for #<Array:0x007fed7949a0c0>
How can I filter down my relation like the select method is doing, but not lose the ability to update/save it the items with ActiveRecord?
Poking around the Google it seems like named_scope's appear in all the answers for similar questions, but I can't figure out it they can specifically accomplish what I'm after.
The problem is that select is not an SQL method. It fetches all records and filters them on the Ruby side. Here is a simplified example:
votes = Vote.scoped
votes.select{ |v| v.active? }
# SQL: select * from votes
# Ruby: all.select{ |v| v.active? }
Since update_all is an SQL method you can't use it on a Ruby array. You can stick to performing all operations in Ruby or move some (all) of them into SQL.
votes = Vote.scoped
votes.select{ |v| v.active? }
# N SQL operations (N - number of votes)
votes.each{ |vote| vote.update_attribute :ignore, false }
# or in 1 SQL operation
Vote.where(id: votes.map(&:id)).update_all(ignore: false)
If you don't actually use fetched votes it would be faster to perform the whole select & update on SQL side:
Vote.where(active: true).update_all(ignore: false)
While the previous examples work fine with your select, this one requires you to rewrite it in terms of SQL. If you have set up all relationships in Rails models you can do it roughly like this:
entry = Entry.find(params[:entry_id])
current_user.votes.joins(:challenges).merge(entry.challenge.votes)
# requires following associations:
# Challenge.has_many :votes
# User.has_many :votes
# Vote.has_many :challenges
And Rails will construct the appropriate SQL for you. But you can always fall back to writing the SQL by hand if something doesn't work.
Use collection_select instead of select. collection_select is specifically built on top of select to return ActiveRecord objects and not an array of strings like you get with select.
#previous_votes = current_user.votes.collection_select { |v| v.entry.challenge_id == Entry.find(params[:entry_id]).challenge_id }
This should return #previous_votes as an array of objects
EDIT: Updating this post with another suggested way to return those AR objects in an array
#previous_votes = current_user.votes.collect {|v| records.detect { v.entry.challenge_id == Entry.find(params[:entry_id]).challenge_id}}
A nice approach this is to use scopes. In your case, you can set this up the scope as follows:
class Vote < ActiveRecord::Base
scope :for_challenge, lambda do |challenge_id|
joins(:entry).where("entry.challenge_id = ?", challenge_id)
end
end
Then your code for getting current votes will look like:
challenge_id = Entry.find(params[:entry_id]).challenge_id
#previous_votes = current_user.votes.for_challenge(challenge_id)
I believe you can do something like:
#entry = Entry.find(params[:entry_id])
#previous_votes = Vote.joins(:entry).where(entries: { id: #entry.id, challenge_id: #entry.challenge_id })

How do I calculate the most popular combination of a order lines? (or any similar order/order lines db arrangement)

I'm using Ruby on Rails. I have a couple of models which fit the normal order/order lines arrangement, i.e.
class Order
has_many :order_lines
end
class OrderLines
belongs_to :order
belongs_to :product
end
class Product
has_many :order_lines
end
(greatly simplified from my real model!)
It's fairly straightforward to work out the most popular individual products via order line, but what magical ruby-fu could I use to calculate the most popular combination(s) of products ordered.
Cheers,
Graeme
My suggestion is to create an array a of Product.id numbers for each order and then do the equivalent of
h = Hash.new(0)
# for each a
h[a.sort.hash] += 1
You will naturally need to consider the scale of your operation and how much you are willing to approximate the results.
External Solution
Create a "Combination" model and index the table by the hash, then each order could increment a counter field. Another field would record exactly which combination that hash value referred to.
In-memory Solution
Look at the last 100 orders and recompute the order popularity in memory when you need it. Hash#sort will give you a sorted list of popularity hashes. You could either make a composite object that remembered what order combination was being counted, or just scan the original data looking for the hash value.
Thanks for the tip digitalross. I followed the external solution idea and did the following. It varies slightly from the suggestion as it keeps a record of individual order_combos, rather than storing a counter so it's possible to query by date as well e.g. most popular top 10 orders in the last week.
I created a method in my order which converts the list of order items to a comma separated string.
def to_s
order_lines.sort.map { |ol| ol.id }.join(",")
end
I then added a filter so the combo is created every time an order is placed.
after_save :create_order_combo
def create_order_combo
oc = OrderCombo.create(:user => user, :combo => self.to_s)
end
And finally my OrderCombo class looks something like below. I've also included a cached version of the method.
class OrderCombo
belongs_to :user
scope :by_user, lambda{ |user| where(:user_id => user.id) }
def self.top_n_orders_by_user(user,count=10)
OrderCombo.by_user(user).count(:group => :combo).sort { |a,b| a[1] <=> b[1] }.reverse[0..count-1]
end
def self.cached_top_orders_by_user(user,count=10)
Rails.cache.fetch("order_combo_#{user.id.to_s}_#{count.to_s}", :expiry => 10.minutes) { OrderCombo.top_n_orders_by_user(user, count) }
end
end
It's not perfect as it doesn't take into account increased popularity when someone orders more of one item in an order.

Is it possible to delete_all with inner join conditions?

I need to delete a lot of records at once and I need to do so based on a condition in another model that is related by a "belongs_to" relationship. I know I can loop through each checking for the condition, but this takes forever with my large record set because for each "belongs_to" it makes a separate query.
Here is an example. I have a "Product" model that "belongs_to" an "Artist" and lets say that artist has a property "is_disabled".
If I want to delete all products that belong to disabled artists, I would like to be able to do something like:
Product.delete_all(:joins => :artist, :conditions => ["artists.is_disabled = ?", true])
Is this possible? I have done this directly in SQL before, but not sure if it is possible to do through rails.
The problem is that delete_all discards all the join information (and rightly so). What you want to do is capture that as an inner select.
If you're using Rails 3 you can create a scope that will give you what you want:
class Product < ActiveRecord::Base
scope :with_disabled_artist, lambda {
where("product_id IN (#{select("product_id").joins(:artist).where("artist.is_disabled = TRUE").to_sql})")
}
end
You query call then becomes
Product.with_disabled_artist.delete_all
You can also use the same query inline but that's not very elegant (or self-documenting):
Product.where("product_id IN (#{Product.select("product_id").joins(:artist).where("artist.is_disabled = TRUE").to_sql})").delete_all
In Rails 4 (I tested on 4.2) you can almost do how OP originally wanted
Application.joins(:vacancy).where(vacancies: {status: 'draft'}).delete_all
will give
DELETE FROM `applications` WHERE `applications`.`id` IN (SELECT id FROM (SELECT `applications`.`id` FROM `applications` INNER JOIN `vacancies` ON `vacancies`.`id` = `applications`.`vacancy_id` WHERE `vacancies`.`status` = 'draft') __active_record_temp)
If you are using Rails 2 you can't do the above. An alternative is to use a joins clause in a find method and call delete on each item.
TellerLocationWidget.find(:all, :joins => [:widget, :teller_location],
:conditions => {:widgets => {:alt_id => params['alt_id']},
:retailer_locations => {:id => #teller_location.id}}).each do |loc|
loc.delete
end

Application level filtering/data manipulation

I have a model with many different children (has_many).
My app needs to lots of different manipulation on the who set of data. Therefore getting the data from the database is pretty simple, so I won't really need to use scopes and finders, BUT I want to do things on the data that are equivalent to say:
named_scope :red, :conditions => { :colour => 'red' }
named_scope :since, lambda {|time| {:conditions => ["created_at > ?", time] }}
Should I be writing the equivalent methods that just manipulate the already served data? Or in a helper?
Just need a little help as most things I see all relate to querying the actual database for a subset of data, when I will require all the children of this one model, but do many different visualisations on it.
So, if I understand right, you'd like to query the whole set of data once, then select different sets of rows from it for different uses.
Named scopes won't do any caching, as they are building separate queries for each variation.
If you want a simple, you can just query all rows (ActiveRecord will cache the result for the same query), then you can use select to filter the rows:
Article.all.select{|a| a.colour == 'red'}
Or, one step further, you can create a general method that filters the rows based on parameters:
def self.search(options)
articles = Articles.all
articles = articles.select{|a| a.color == 'red'} if options[:red]
articles = articles.select{|a| a.created_at > options[:since]} if options[:since]
articles
end
Article.search(:red => true, :since => 2.days.ago)
Or, if you really want to keep the chainable method syntax provided by scopes, then add your filter methods to the Array class:
class Array
def red
select.select{|a| a.colour == 'red'}
end
end
Or, if you just don't want to add all that garbage to every Array object, you can just add them to the objects, but you'll need to override the all method and add the methods every time you're creating a subset of the rows:
def self.with_filters(articles)
def articles.red
Article.with_filters(select{|a| a.color == 'red'})
end
articles
end
def self.all
self.with_filters(super)
end

Resources