Using state attributes to maintain old records in Rails - ruby-on-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

Related

Active Record Scope modifies Active Record Relation. Why?

Applying a scope to a an Active Record Relation is permanently modifying the relation. Why?
company_purchases.to_sql
=> "SELECT \"purchases\".* FROM \"purchases\" WHERE \"purchases\".\"company_id\" = 17"
company_purchases.by_state("finalized").to_sql
=> "SELECT \"purchases\".* FROM \"purchases\" WHERE \"purchases\".\"company_id\" = 17 AND \"purchases\".\"state\" = 'finalized'"
company_purchases.to_sql
=> "SELECT \"purchases\".* FROM \"purchases\" WHERE \"purchases\".\"company_id\" = 17 AND \"purchases\".\"state\" = 'finalized'"
I expect the SQL to look different when called on the scope, but I don't understand why the additional where from the scope remains on the next call to company_purchases without the scope.
The scope definition
scope :by_state, ->(state) { where(state: state) }
UPDATE
This appears to be a bug with the gem Octopus, see here: https://github.com/thiagopradi/octopus/issues/455
For additional context, the Octopus bug is being introduced because of how company_purchases is composed.
company_purchases = company.purchases
# in Company model
def purchases
Product.using(shard).where(company_id: id)
end
If you default_scope than it will display on every query you make on that model. Instead use scope to avoid above problem.
This appears to be an issue with Octopus, not Active Record scopes or relations.
See: https://github.com/thiagopradi/octopus/issues/455

Creating a record not found through default_scope

Consider the model
class Product < ActiveRecord::Base
scope :queued, lambda { where(queued: true) }
scope :unqueued, lambda { where(queued: false) }
default_scope unqueued
end
Product.first yields the SQL query
SELECT "products".* FROM "products" WHERE "products"."queued" = 'f'
LIMIT 1
Now what if I want to create a record that is not "true to the default scope?" Like so:
Product.queued.create!
The product is in fact created, but ActiveRecord yields an error since it tries to find the product by it's id AND the default scope:
ActiveRecord::RecordNotFound:
Couldn't find Product with id=15 [WHERE "products"."queued" = 'f']
Is there a workaround for this? I need to make sure that the product I create is queued. A simple workaround would be
p = Product.create
p.update_column(:queued, true)
It seems like the wrong answer to another problem though, or perhaps it is the right answer. Are there alternatives?
Thanks for your time.
The best solution would be to not use default_scope. default_scope should only be used when you always need to apply the scope when searching for records. So, if you ever need to find a record where queued is true, then you should not be using default_scope.
One other way to get past default_scope is to use the unscoped method, i.e.:
Product.unscoped.queued
But, in general, if you need to use ActiveRecord to find queued Products, I would recommend removing your default_scope.

Rails 3 - how to add to autocomplete another condition?

I use the gem rails3-jquery-autocomplete for the autocomplete searching of items in my database. The autocomplete is working fine, but I would need to add another condition to the generated query.
If I start to write a searched string, then is generated following query:
SELECT persons.id, persons.name FROM "persons" WHERE (LOWER(persons.name) ILIKE 'jo%') ORDER BY persons.name ASC LIMIT 10
This returns me all rows, where the name starts at jo.
But how could I search all persons, which name starts at jo and simultaneously, for example, the column active_person=1?
Is there any helper for this purpose or something like that?
Thank you
If I understood the context correctly, then active_person is a boolean that indicates whether the user is active or not.
If this behavior is desirable most of time - that you search for users/people that are only active then you could include a default_scope in your Person model like this:
class Person < ActiveRecord::Base
default_scope where(:active_person => 1)
end
This way any query that is ever generated will be also checking that active_person = 1
More on default scopes here: http://apidock.com/rails/ActiveRecord/Base/default_scope/class
I understand that #Andrei 's answer is correct, but its not what I was looking for. I guess its same for #user984621 too. What I was looking for is I want to add an extra condition to the query.
By reading this https://github.com/crowdint/rails3-jquery-autocomplete/blob/master/lib/rails3-jquery-autocomplete/orm/active_record.rb
I found that rails autocomplete supports additional conditions, you just have to pass another parameter to the default usage, like this
autocomplete :user, :name, :where => { :role => 'employee'}
instead of this
autocomplete :user, :name
Change WHERE (LOWER(persons.name) ILIKE 'jo%') to WHERE (LOWER(persons.name) ILIKE 'jo%') AND active_person = 1
You can chain together loads of clauses with ANDs and OR in that way.
Note that this is really a question about SQL: your title and tags are a bit misleading.

Select the complement of a set

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 :-)

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.

Resources