Best Practice to abstract ActiveRecord model queries in Rails? - ruby-on-rails

I'd like to extract out logic from the controllers to somewhere that it can be more DRY. What's the best way of handling something like the following in Rails?
For instance, as opposed to having the query in the controllers, I could place it in the model:
class SmoothieBlender < ActiveRecord::Base
belongs_to :user
def self.get_blenders_for_user(user)
self.where(["user_id = ?", user.id])
end
end
Or would it be better to create a module as a service layer and include that in each model that uses it?
module BlenderUser
def get_blenders_for_user(user)
SmoothieBlender.where(["user_id = ?", user.id])
end
end
class SmoothieBlender < ActiveRecord::Base
include BlenderUser
belongs_to :user
end
class User < ActiveRecord::Base
include BlenderUser
has_many :smoothie_blenders
end
Or just make it a full blown service class that's accessible from the User and Blender controller? Where would you put this class?
class BlenderService
def self.get_blenders_for_user(user)
SmoothieBlender.where(["user_id = ?", user.id])
end
end
I'm new to Ruby and Rails, so if this is a silly question/syntax is incorrect, forgive me. Thanks in advance!

I'd create a named_scope (I think it's just scope in Rails 3)
class SmoothieBlender < ActiveRecord::Base
belongs_to :user
scope :for_user, lambda { |user_id|
where("user_id = ?", user_id)
}
end
This way you can call
SmoothieBlender.for_user(user.id)

Related

Rails: filter association based on current_user class

Have an app where current_user can be a Client or and Admin. A Client can have many Accounts and an AccountStatement belongs to both the Account and the Client.
However there are some auto generated statements that we don't want to show the clients. I'd like to add a model scope of some kind that would look something like
def account_statements
if current_user.is_a?(Client)
super.where(auto_generated: false)
else
super
end
end
so if I ran .account_statements on a valid instance of Account or Client it would only return a subset of the statements if the current_user is a Client, but all of them if the current_user is an Admin. Is there any way to do this?
Just give each class its own method:
class Admin < ApplicationRecord
...
def account_statements
AccountStatement.all
end
end
class Client < ApplicationRecord
...
def account_statements
AccountStatement.where(auto_generated: false)
end
end
If you need Client to have a defined relationship to AccountStatement, things get a little more tricky:
class Client < ApplicationRecord
has_many :account_statements
# this method will supersede the relationship
def account_statements
...
end
end
In which case, you can either:
use a different name for the method
use this cool relationship-with-scope trick to do something like this:
class Client < ApplicationRecord
has_many :account_statements, -> { without_auto_generated }
end
class AccountStatement < ApplicationRecord
belongs_to :client
belongs_to :account
scope :without_auto_generated, -> { where(auto_generated: false) }
end

Iterate over STI list in rails 4

I have two sub models, called: Service and Product that inherits from ProductBase. And I have another model to consume it. Acquire that have many AcquireBasket. Check out my code:
product_base.rb:
class ProductBase < ActiveRecord::Base
extend ::EnumerateIt
include Searchable
self.table_name = 'products'
end
product.rb:
class Product < ProductBase
default_scope { where(kind: ProductKind::PRODUCT) }
def initialize(attributes = {})
super(attributes)
self.kind = ProductKind::PRODUCT
self.status = ProductStatus::DRAFT
end
end
service.rb:
class Service < ProductBase
default_scope { where(kind: ProductKind::SERVICE) }
def initialize(attributes = {})
super(attributes)
self.kind = ProductKind::SERVICE
self.status = ProductStatus::DRAFT
end
end
acquire_basket.rb:
class AcquireBasket < ActiveRecord::Base
extend ::EnumerateIt
belongs_to :acquire
belongs_to :product
end
In some part of my project, I get a list (acquire baskets) of both models, Service and Product. And I need to check if I have services inside of it.
My code to check was:
def services_in?(acquire)
acquire.baskets.map(&:product).detect(&:service?)
end
The code works, ONLY if I pass services first, and products after!! Or if I have only one of them.
You should be able to utilize the descendents method to iterate over all of the subclasses
I can't find the answer in blog post around the world, so I will share with you:
class AcquireBasket < ActiveRecord::Base
extend ::EnumerateIt
belongs_to :acquire
belongs_to :product, class_name: 'ProductBase'
end
The problem was, when I try to find (lazily) in a ActiveRecord::Relation, Rails lookup (I think) to just Product model. And It can't find other type models inside of it. So using this typo I put it to work.

Access Controller Variable on default_scope

My app is based on the subdomain. Each manager has it's own subdomain that should show only his auctions. I have:
class Lot < ActiveRecord::Base
belongs_to :auction
end
class Auction < ActiveRecord::Base
has_many :lots
belongs_to :manager
end
class manager < ActiveRecord::Base
has_many :auctions
end
If the app was accessed using a subdomain, I have a before_filter that do the following:
def load_manager
#loaded_manager = Manager.find_by_subdomain(request.subdomain)
end
and on my Lot's default_scope, i'd like to do the following:
default_scope { #loaded_manager.present? ? where(deleted: false).joins(:auction => :manager).where("auctions.manager_id = ?", #loaded_manager.id) : where(deleted: false) }
So that way wherever I'm on the website I'll just show the lots that belongs to the manager's auctions.
The problem is that I can't access #loaded_manager on the model. What's the best way for doing this?
http://railscasts.com/episodes/388-multitenancy-with-scopes?view=comments solves this issue!
You may store current manager in Manager model in before filter:
def load_manager
Manager.set_current(request.subdomain)
end
class Manager < ActiveRecord::Base
cattr_accessor :current
def self.set_current(subdomain)
self.current = self.find_by_subdomain(subdomain)
end
end
class Lot < ActiveRecord::Base
default_scope { Manager.current.present? ? where(deleted: false).joins(:auction => :manager).where("auctions.manager_id = ?", Manager.current.id) : where(deleted: false) }
end
Update
As #Mik_Die noticed it is not thread safe, for thread safe solution a reader may look at railscasts - multitenancy-with-scopes (code here). There we just store current_id in Thread.
Models don't have access to controller's instance variables (and should not have - it breaks MVC principles)
Probably, using default scope in this case is bad idea. Consider using usual scope like this
scope :for_manager, lambda{ |manager| manager.present? ? where(deleted: false).joins(:auction => :manager).where("auctions.manager_id = ?", manager.id) : where(deleted: false) }
And then in your controller
#lots = Lot.for_manager(#loaded_manager)

Rails: How can I eager load associations with sorting through instance an method?

class Newsroom < ActiveRecord::Base
has_many :blog_posts
has_many :quote_posts
end
class BlogPost < ActiveRecord::Base
belongs_to :newsroom
end
class QuotePost < ActiveRecord::Base
belongs_to :newsroom
end
I would like to have an instance method, such that I could do #newsroom.posts to get a collection of blog_posts and quote_posts sorted by created_at.
def posts
#posts ||= #load and sort blog_posts, quote_posts, etc
end
What is the best and most efficient way to accomplish this? I have looked into using default_scope, something like:
default_scope :include => [:blog_posts, :quote_posts]
def posts
#posts ||= [blog_posts + quote_posts].flatten.sort{|x,y| x.created_at <=> y.created_at}
end
But I would rather keep the sorting at the database level, if possible. Any suggestions on how to accomplish this? Thanks.
Try something like this:
#app/models/newsroom.rb
scope :ordered_posts, lambda {
includes(:blog_posts,:quote_posts) & BlogPost.order("created_at asc") & QuotePost.order("created_at asc")
}
ARel should be able to handle the ordering of included Quote and Blog Posts. You could clean that up slightly by having scopes in both the BlogPost and QuotePost model that order by created_at and then use those scopes in the Newsroom#ordered_posts method.
I ended up using a polymorphic post model. This seems to give me what I want with the insignificant downside of having an extra model/table. I used delegate to hand off specific attribute getter methods to the correct model.
class Newsroom < ActiveRecord::Base
has_many :posts
end
class Post < ActiveRecord::Base
belong_to :blog_post, :polymorphic => true
delegate :title, :author, :etc, :to => :postable
end
class BlogPost < ActiveRecord::Base
has_one :post, :as => :postable
end

Rails tip - "Use model association"

So, I've read in some book about tip "Use model association", which encourages developers to use build methods instead of putting ids via setters.
Assume you have multiple has_many relationships in your model. What's best practise for creating model then ?
For example, let's say you have models Article, User and Group.
class Article < ActiveRecord::Base
belongs_to :user
belongs_to :subdomain
end
class User < ActiveRecord::Base
has_many :articles
end
class Subdomain < ActiveRecord::Base
has_many :articles
end
and ArticlesController:
class ArticlesController < ApplicationController
def create
# let's say we have methods current_user which returns current user and current_subdomain which gets current subdomain
# so, what I need here is a way to set subdomain_id to current_subdomain.id and user_id to current_user.id
#article = current_user.articles.build(params[:article])
#article.subdomain_id = current_subdomain.id
# or Dogbert's suggestion
#article.subdomain = current_subdomain
#article.save
end
end
Is there a cleaner way ?
Thanks!
This should be a little cleaner.
#article.subdomain = current_subdomain
The only thing I can think of is merging the subdomain with params:
#article = current_user.articles.build(params[:article].merge(:subdomain => current_subdomain))

Resources