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.
Related
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
I'm building a system that has some records in tables that are template records, which are viewable by all accounts and can be later copied to create live records for an individual account.
The reasoning behind this design decision is that template records and live records share 95%+ of the same code, so I didn't want to create a separate table to track mostly the same fields.
For instance, I have a workflows table:
id:integer
account_id:integer
name:string (required)
is_a_template:boolean (default: false)
is_in_template_library:boolean (default: false)
In this table, I have some records that are templates. When I go to create a new live record, I can use a template record:
# workflows_controller.rb (pseudo-code, not fully tested)
def create
#workflow_template = Workflow.where(is_a_template: true).find_by(id: params[:workflow_template_id])
#workflow = current_account.workflows.new(workflow_params.merge(#workflow_template.dup))
if #workflow.save
...
else
...
end
end
As I build more functionality, I find that I really need 2 different models that operate differently on the table. There are several more differences, but those listed below are enough to show the differences:
class Workflow < ApplicationRecord
default_scope -> { where(is_a_template: false) }
belongs_to :account
validates :account, presence: true
validates :name, presence: true
end
class WorkflowTemplate < ApplicationRecord
default_scope -> { where(is_a_template: true) }
validates :name, presence: true
end
class WorkflowLibraryTemplate < ApplicationRecord
default_scope -> { where(is_a_template: true, is_in_template_library: true) }
validates :name, presence: true
end
As you can see, the workflows table has 3 different "types" of records:
"live" workflows that belong to an account
template workflows that also belong to an account and are copied to create "live" workflows
library template workflows that do NOT belong to an account and can be viewed by any account, so they can copy them into their own list of templates
Question
What I'm trying to figure out, is at what point do I break up this single table into multiple tables, versus keeping the same table and having multiple models, or what solution is there to a problem like this?
The frustrating part is that there are 5+ other tables that are "children" associations of the workflows table. So if I decide that I need separate tables for each, I would end up going from 6 tables to something like 18, and everytime I add a field, I have to do it to all 3 "versions" of the table.
Thus I'm very reluctant to go down the multiple tables route.
If I keep a single table and multiple models, I then end up with different version of data in the table, which isn't the end of the world. I only interact with the data through my application (or a future API I control).
Another solution I'm thinking about is adding a role:string field to the table, which operates very much like the type field in Rails. I didn't want to use STI, however, because there are too many baked-in requirements with Rails that I don't want to conflict with.
What I'm envisioning is:
class Workflow < ApplicationRecord
scope :templates, -> { where(role: "template") }
scope :library_templates, -> { where(role: "library_template") }
validates :account, presence: true, if: :account_required?
validates :name, presence: true
# If record.role matches one of these, account is required
def account_required
["live", "template"].include?(role.to_s.downcase)
end
end
This seems to address several of the issues, keeps me with 1 table and 1 model, but begins to have conditional logic in the model, which seems like a bad idea to me as well.
Is there a cleaner way to implement a templating system within a table?
So what you are looking at here is called Single Table Inheritance. The models are called polymorphic.
As far as when to break up the STI into distinct tables, the answer is: when you have enough divergence that you start having specialized columns. The problem with STI is that let's say WorkFlows and WorkFlowTemplate start to diverge. Maybe the template starts getting a lot of extra attributes as columns that do not correspond to plain old workflows. Now you have lots of data that is empty for one class (or not needed) and useful and necessary for the other. In this case, I'd probably break the tables apart. The real question you should ask is:
How far will these models diverge from each other in terms of requirements?
How soon will this happen?
If it happens very late in the life of my app:
Will it be difficult/impossible to migrate these tables due to how many rows/how much data I have?
Edit:
Is there a cleaner way? In this specific case, I don't think so given a template and a copy of that template, are likely to be tightly coupled to each other.
The approach I took is decomposition by responsibility.
Decomposition by responsibility:
Right now, you have 3 different sources of data and 2 different ways to create/validate a workflow.
In order to achieve that, you can introduce the concept of Repositories and FormObjects.
Repositories are wrapper objects that will abstract the way you query your model. It doesn't care if it is the same table or multiple. It just knows how to get the data.
For example:
class Workflow < ApplicationRecord
belongs_to :account
end
class WorkflowRepository
def self.all
Workflow.where(is_a_template: false)
end
end
class WorkflowTemplateRepository
def self.all
Workflow.where(is_a_template: true)
end
end
class WorkflowLibraryTemplateRepository
def self.all
Workflow.where(is_a_template: true, is_in_template_library: true)
end
end
This makes sure that no matter what you decide in the future to do, you will not change other parts of the code.
So now let's discuss FormObject
FormObject will abstract the way you validate and build your objects. It might not be a great addition right now but usually, pays off in the long run.
For example
class WorkFlowForm
include ActiveModel::Model
attr_accessor(
:name,
:another_attribute,
:extra_attribute,
:account
)
validates :account, presence: true
validates :name, presence: true
def create
if valid?
account.workflows.create(
name: name, is_a_template: false,
is_in_template_library: false, extra_attribute: extra_attribute)
end
end
end
class WorkflowTemplateForm
include ActiveModel::Model
attr_accessor(
:name,
:another_attribute,
:extra_attribute
)
validates :name, presence: true
def create
if valid?
Workflow.create(
name: name, is_a_template: true,
is_in_template_library: false, extra_attribute: extra_attribute)
end
end
end
class WorkflowLibraryTemplateForm
include ActiveModel::Model
attr_accessor(
:name,
:another_attribute,
:extra_attribute
)
validates :name, presence: true
def create
if valid?
Workflow.create(
name: name, is_a_template: true,
is_in_template_library: true, extra_attribute: extra_attribute)
end
end
end
This approach helps with extendability as everything is a separate object.
The only drawback of that is that In my humble opinion, WorkflowTemplate and WorkflowLibraryTemplate are semantical the same thing with an extra boolean but that's an optional thing you can take or leave.
I'm trying to set a default scope so that Users where notified: true are soft-deleted. notified is a boolean data column.
This is what I've tried:
class User < ActiveRecord::Base
default_scope { where('notified != ?', true) }
#...
end
But this way, no users appear in any scope. ie - all the users appear to be soft-deleted, even the ones with notified: false or notified: nil. What's wrong with my scope?
I recommend using boolean values the database understands. In this case you want to see users that have a notified that is not true, so I'd user:
default_scope { where('notified IS NOT TRUE') }
That way users will only appear in other scopes if their boolean database values is FALSE or NULL.
Note: default scopes are actually considered a code smell... because they're a bit magic and hide away what you really mean when you fetch out users. You might want to instead create an active and inactive scope and specify them explicitly in your code eg:
scope :active ->{ where('notified IS NOT TRUE') }
scope :inactive ->{ where('notified IS TRUE') }
# in your controller
def index
#users = User.active.all
end
According to your objective of 'set a default scope so that Users where notified != true are soft-deleted.', you should use default_scope { where(manual_down: true) }, which will only retrieve the records with that column is TRUE, and ignore the others (FALSE or NIL)
I totally agree with Taryn East. Changing/removing default_scope might require lots of modification to logic that depends on that model, so use it only if you're sure you don't change the default_scope condition later (which is usually not the case).
I wonder what the beautiful way to output this in Rails.
order.items.min.user.email
I want to show the value of email, the only thing I know is order is not nil, however items and user might be nil.
The only way I see is
if !order.items.empty?
if !order.items.min.user.nil?
if !order.items.min.user.email.nil?
order.items.min.user.email
end
end
end
It looks like not the best way, do you know the better way?
You could use try (or try! depending on your Rails3 version and how you want unknown methods to be handled):
order.items.try(:min).try(:user).try(:email)
try is an easy way to swallow nils (and unknown methods depending on your Rails version) in a long method call chain.
Consider this more elegant approach:
Order:
class Order < ActiveRecord::Base
has_many :items
end
Item:
class Item < ActiveRecord::Base
belongs_to :user
delegate :email, to: :user, allow_nil: true, prefix: true # You can remove the prefix true and call item.email instead item.user_email
end
Then:
order.items.min.try(:user_email)
or:
order.items.min.try(:user_email).presence || "Not found"
Which will return the user's email or "Not found" in case items.min is nil, user is nil, or email is nil.
I have the following situation
class RecordA
has_many :recordbs
end
class RecordB
belongs_to :recorda
end
RecordA has many recordbs but only one of them may be an active recordb. I need something like myRecordA.active_recordb
If I add a new column like is_active to RecordB, then I have the potential problem of setting two records to is_active = true at the same time.
Which design pattern can I use?
Thanks!
Let's change your example. There's a LectureRoom, with many People and only one Person can be the instructor.
It'd be much easier to just have an attribute in the LectureRoom to indicate which Person is the instructor. That way you don't need to change multiple People records in order to swap the instructor. You just need to update the LectureRoom record.
I would use a named scope to find the active lecturer.
class Person
named_scope :currently_speaking, :conditions => {:active => true}
end
Then I would call that a lecturer in ClassRoom:
class ClassRoom
def lecturer
people.currently_speaking.first
end
end
The real problem is making sure that when you activate someone else, they become the only active person. I might do that like this:
class Person
belongs_to :class_room
before_save :ensure_one_lecturer
def activate!
self.active = true
save
end
def ensure_one_lecturer
if self.active && changed.has_key?(:active)
class_room.lecturer.update_attribute(:active, false)
end
end
end
This way everything is done in a transaction, it's only done if you've changed the active state, and should be pretty easily tested (I have not tested this).
You can define a class method on RecordB for this:
class RecordB < ActiveRecord::Base
def self.active
first(:conditions => { :active => true }
end
end