How do you set up chainable scopes based on relations in Mongoid - ruby-on-rails

Problem fixed... Turned out there was an active record method that got over written, now everything works as expected
I am trying to set up scopes so I can make a call that looks like
Competitor.of_type(type).at_event(event)
that will return all Competitors of type that attended event
My Models looks something like
class Competitor < Competitor
belongs_to :type
has_and_belongs_to_many :events
scope :at_event, ->(event) {where(:event_ids.in => event.competitor_ids)}
scope :of_type, ->(type) where(:type_id => type.id)
end
The following works (return mongoid criteria)
Competitor.of_type(type)
Competitor.at_event(event)
But when I chain them, it prints out something that looks like this:
#<Competitor:0x00000109e2b210>
#<Competitor:0x00000109e2ab08>
-------=-=------------------------------------
=> #<Mongoid::Criteria
selector: {},
options: {},
class: Competitor,
embedded: false>
There is a Competitor entry for each of Competitor.of_type(type) (the first chained criteria) and if I run .count on the query, I get the total number of Competitors in the database.
At the top of the mongoid documentation for scopes, it says All scopes are chainable and can be applied to associations as well, the later being discussed in the relations section.
Unfortunately I did not see a relations sub section, not could I find a single reference to scope in the main relations section.
I was able to get the following to return the results I wanted:
where(:id.in => event.competitor_ids).where(:type_id => type.id)
but if any part of the query is split into a separate method or scope it fails and provides the result I showed above.

scopes
Similar to Active Record, Mongoid allows you to define scopes on your
models as a convenience for filtering result sets. Scopes are defined
at the class level, either using the scope macro or by defining class
methods that return a criteria object. All scopes are chainable and
can be applied to associations as well, the later being discussed in
the relations section.
Named scopes are defined at the class level using a scope macro and can be chained to create result sets in a nice DSL.
class Person
include Mongoid::Document
field :occupation, type: String
field :age, type: Integer
scope :rock_n_rolla, where(occupation: "Rockstar")
scope :washed_up, where(:age.gt => 30)
scope :over, ->(limit) { where(:age.gt => limit) }
end
# Find all the rockstars.
Person.rock_n_rolla
# Find all rockstars that should probably quit.
Person.washed_up.rock_n_rolla
# Find a criteria with Keith Richards in it.
Person.rock_n_rolla.over(60)
Note that definitions are evaluated at class load time. For
evaluation at runtime you will want to make sure to define using a
proc or lambda. In the following example the first date is set as the
date of class load, where the second scope sets the date at the time
the scope is called.
scope :current, where(:start_date.lte => Date.today)
scope :current, -> { where(:start_date.lte => Date.today) }
class methods
For those who prefer a Data Mapper style syntax, class methods that return criteria can be treated as chainable scopes as well.
class Person
include Mongoid::Document
field :occupation, type: String
field :age, type: Integer
class << self
def rock_n_rolla
where(occupation: "Rockstar")
end
def washed_up
where(:age.gt => 30)
end
def over(limit)
where(:age.gt => limit)
end
end
end
# Find all the rockstars.
Person.rock_n_rolla
# Find all rockstars that should probably quit.
Person.washed_up.rock_n_rolla
# Find a criteria with Keith Richards in it.
Person.rock_n_rolla.over(60)
Named scopes and class methods that return a criteria can be chained together - that's the beauty of Mongoid's powerful criteria API.
class Person
include Mongoid::Document
field :occupation, type: String
field :age, type: Integer
scope :washed_up, where(:age.gt => 30)
scope :over, ->(limit) { where(:age.gt => limit) }
def self.rock_n_rolla
where(occupation: "Rockstar")
end
end
# Same queries apply here as well.
Person.rock_n_rolla
Person.washed_up.rock_n_rolla
Person.rock_n_rolla.over(60)

Although #MZaragoza's answer was complete, it seems that this syntax is no longer allowed:
scope :rock_n_rolla, where(occupation: "Rockstar")
Use procs instead:
summary:
Scopes in Mongoid must be procs that wrap criteria objects.
resolution:
Change the scope to be a proc wrapped critera.
Example:
  class Band
    include Mongoid::Document
    scope :inactive, ->{ where(active: false) }
  end
Mongoid v 7.0.3

Related

How can i write a method that can generate scope dynamically which can be used for multiple models in ruby on rails

Currently, the existing scopes are like this.
module TransactionScopes
extend ActiveSupport::Concern
included do
scope :status, ->(status) { where status: status }
scope :portfolio_id, ->(portfolio_id) { where portfolio_id: portfolio_id }
scope :investor_external_reference_id, ->(investor_external_reference_id) { where investor_external_reference_id: investor_external_reference_id }
scope :portfolio_external_reference_id, ->(portfolio_external_reference_id) { where portfolio_external_reference_id: portfolio_external_reference_id }
scope :file_id, ->(file_id) { where back_office_file_id: file_id }
scope :oms_status, ->(status) { where oms_status: status }
scope :order_id, ->(order_id) { where order_id: order_id }
scope :order_status, ->(order_status) { where order_status: order_status }
scope :transaction_id, ->(transaction_id) { where transaction_id: transaction_id }
end
I have some more models with similar scopes, can I write more generic way so I can avoid these repeated processes.
I strongly discourage you from adding a scope for each single attribute. where(...) is only 5 characters and provides additional context to the reader. Person.where(name: 'John Doe') says: on Person execute a query (where) and return a collection that matches the criteria name: 'John Doe'.
If you add the suggest attribute scope the line becomes Person.name('John Doe'). By removing the context that this is a query a reader must "learn" that each attribute name can also be accessed as a scope.
The above immediately shows another issue, which is name conflicts. Person.name is already taken, and returns the class name. So adding scope :name, ->(name) { where(name: name) } will raise an ArgumentError.
Scopes can be useful, but when used too much they clutter the class method namespace of the model.
With the above out of the way, here are some actual solutions.
You could write a helper that allows you to easily create scopes for attributes. Then loop through the passed attributes and dynamically create scopes for them.
class ApplicationRecord < ActiveRecord::Base
class << self
private
def attribute_scope_for(*attributes)
attributes.each { |attr| scope attr, ->(value) { where(attr => value) } }
end
end
end
Then call this helper in your models.
class YourModel < ApplicationRecord
attribute_scopes_for :status, :portfolio_id # ....
end
Alternatively, if you want to create a scope for each attribute you can dynamically collect them using attribute_names. Then loop through them and create scopes based on the names.
class ApplicationRecord < ActiveRecord::Base
class << self
private
def enable_attribute_scopes
attribute_names
.reject { |attr| respond_to?(attr, true) }
.each { |attr| scope attr, ->(value) { where(attr => value) } }
end
end
end
In the above snippet .reject { |attr| respond_to?(attr, true) } is optional, but prevents the creation of scopes that have a name conflict with a current public/private class method. This will skip those attributes. You can safely omit this line, but the scope method might raise an ArgumentError when passing dangerous scope names.
Now the only thing left to do is calling enable_attribute_scopes in models where you want to enable attribute scopes.
The above should give you an idea of how you could handle things, you could even add options like :except or :only. There is also the option to extract the code above into a module and extend AttributeScopeHelpers within ApplicationRecord if the class becomes to cluttered.
However, like I started this answer I would advise against adding scopes for each attribute.

How to add condition for all where query for an ActiveRecordModel?

I have a user table in my rails application and the application uses many where conditions for this model throughout the application in many controller methods.
Now i have to add an extra attribute for the where condition.
is there a way to do the following and how? instead of adding the extra attribute to all the where condition used in the entire application can i write a custom where to the user model so the condition will be pre-added to the where in entire application for the user model.
i found out the source for the where
def where(opts = :chain, *rest)
if :chain == opts
WhereChain.new(spawn)
elsif opts.blank?
self
else
spawn.where!(opts, *rest)
end
end
my where condition in the controller methods now:
User.where(:status => true, :country => "IN")
this condition and similar conditions are used in many methods in application and i want to get the user who has not :deactivated.
i can make changes to all where condition like
User.where(:status => true, :country => "IN", :deactivated => false)
instead i thought of writing a custom where that precheck :deactivated => false
Default Scope:
class User < ActiveRecord::Base
default_scope -> { where(deactivated: false) }
end
You can use default_scope.
Now, whenever you query User, automatically the default scope query will get appended.
For more details on default_scope, please refer:
https://api.rubyonrails.org/classes/ActiveRecord/Scoping/Default/ClassMethods.html#method-i-default_scope
If there are usecases that prevent you from using default_scope, then you can use custom scopes or unscope the default scope.
Unscoping:
You can unscope in Project model if you want to remove the default scope.
belongs_to :user, ->{ unscope(where: :deactivated) }
Or you can fetch all user and then unscope
project.users.unscoped
Custom Scope:
class User < ActiveRecord::Base
scope :deactivated, ->(deactivated = false) { where(deactivated: deactivated) }
end
Now, to make use of that scope, you can query like this:
User.deactivated.where(:status => true, :country => "IN")
For reference:
https://api.rubyonrails.org/classes/ActiveRecord/Scoping/Named/ClassMethods.html#method-i-scope

model association with a scope for trashable module

I have a trashable concern that allows a user to trash ("delete") certain things.
The issue is that even though that item can be trashed, it still has to be referenced if you view something older. If you do that now it won't find that object as I've changed the default_scope to only show where trashed is false.
Here's my trashable module:
module Trashable
extend ActiveSupport::Concern
included do
default_scope { where(trashed: false) }
scope :trashed, -> { unscoped.where(trashed: true) }
validates :trashed, inclusion: { in: [true, false] }
end
def trash
update_attribute :trashed, true
end
end
now I have an Order model, where you can view an order. If we for example trash a product, I still want the user to be able to look at their order and see the product.
Now I'm not able to access that with a model association such as:
has_many :products and make it so that it includes both where trashed is false and true.
Does anybody know how to achieve this?
You can achieve this by several ways, here is what I know
Solution 1 Define with_trashed like this:
module Trashable
extend ActiveSupport::Concern
included do
default_scope { where(trashed: false) }
scope :trashed, -> { unscoped.where(trashed: true) }
# Define a new scope here
scope :with_trashed, -> { unscope(where: :trashed) }
validates :trashed, inclusion: { in: [true, false] }
end
def trash
update_attribute :trashed, true
end
end
Then you can use it like:
order.products.with_trashed
Solution 2 Define unscoped class
class UnscopedProduct < Product
self.default_scopes = []
belongs_to :order
end
class Order < ActiveRecord::Base
has_many :products
has_many :unscoped_products, foreign_key: :order_id, class_name: "UnscopedProduct"
end
Then you can use it like:
order.unscope_products
As my experience, I would use Solution 1, but there are some weird cases, this doesn't work anymore, for example, in a complex query, so remember solution 2, it will save a lot of time!
Using Default scope will lead to so many problems for later complex queries. That depends on you!
You can do a couple of things:
As someone mentioned in the comment, you should use Acts as Paranoid Gem that is exactly meant for this purpose. Using that, you can use methods with_deleted or only_deleted that will return you relevant deleted objects as well.
You cannot simply use unscoped as you are doing above. Unscoped will remove all the conditions and not only trashed: false. You can also try to create another scope which returns you deleted objects and merge the objects found in the second scope with the first one.
scope_1_results + scope_2_results
If you are on Rails 5, you can also OR the scopes which is not possible in Rails 4.x or less.

Mongoid named scope comparing two time fields in the same document

I need to create a named scope in Mongoid that compares two Time fields within the same document. Such as
scope :foo, :where => {:updated_at.gt => :checked_at}
This obviously won't work as it treats :checked_at as a symbol, not the actual field. Any suggestions on how this can be done?
Update 1
Here is my model where I have this scope declared, with a lot of extra code stripped out.
class User
include Mongoid::Document
include Mongoid::Paranoia
include Mongoid::Timestamps
field :checked_at, :type => Time
scope :unresolved, :where => { :updated_at.gt => self.checked_at }
end
This gives me the following error:
'<class:User>': undefined method 'checked_at' for User:Class (NoMethodError)
As far as I know, mongodb doesn't support queries against dynamic values.
But you could use a javascript function:
scope :unresolved, :where => 'this.updated_at >= this.checked_at'
To speed this up you could add an attribute like "is_unresolved" which will be set to true on update when this condition is matched ( and index that ).
scope :foo, :where => {:updated_at.gt => self.checked_at}
For example, this will work:
scope :foo, where(:start_date.lte=>Date.today.midnight)
Not sure if you'll like this method, it's not the best, but it should work.
class User
include Mongoid::Document
include Mongoid::Paranoia
include Mongoid::Timestamps
field :checked_at, :type => Time
scope :unresolved, lambda{ |user| where(:updated_at.gt => user.checked_at) }
end
You call it with User.unresolved(my_user_object)
It seems now after rereading your post that this probably won't do what you want. If this is true, then you will probably need to use MapReduce or possibly Baju's method (have not tested it)

How to use common named_scope for all ActiveRecord models

Hi how to build a named_scope which will be common for all models.
I do that by putting this code in lib/has_common_named_scopes.rb:
module HasCommonNamedScopes
def self.included(base)
base.class_eval {
# Named scopes
named_scope :newest, :order => "#{base.table_name}.created_at DESC"
named_scope :freshest, :order => "#{base.table_name}.updated_at DESC"
named_scope :limit, lambda { |limit| {:limit => limit} }
}
end
end
and then include the module in each model where I need them:
class MyModel < ActiveRecord::Base
include HasCommonNamedScopes
I'd recommend that you use base.table_name to qualify the table when referring to columns in these named scopes like I do in the example. Otherwise you run into problems with ambiguous references when you combine these named scopes with other scopes that join in other tables.
Update:
scope is used in Rails > 3 and named_scope was used in previous versions.
There's also Thoughtbot's Pacecar, which adds a bunch of very common named scopes to every model. It might come with what you're looking for. If you need something custom, though, Casper Fabricius has the right idea.
For a Rails4 project I achieved this by extending ActiveRecord::Base, the class all Rails models inherit from, in an initializer (monkey patching approach, beware)
# in /config/initializers/shared_scope_initializer.rb
module SharedScopes
extend ActiveSupport::Concern
module ClassMethods
def named_scope
return where(attribute: value) # query here
end
end
end
ActiveRecord::Base.send(:include, SharedScopes)

Resources