public
Last active

Use concerns to keep your models manageable

  • Download Gist
gistfile1.rb
Ruby
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
# autoload concerns
module YourApp
class Application < Rails::Application
config.autoload_paths += %W(
#{config.root}/app/controllers/concerns
#{config.root}/app/models/concerns
)
end
end
 
# app/models/concerns/trashable.rb
module Trashable
extend ActiveSupport::Concern
included do
default_scope where(trashed: false)
scope :trashed, where(trashed: true)
end
def trash
update_attribute :trashed, true
end
end
 
# app/models/message.rb
class Message < ActiveRecord::Base
include Trashable, Subscribable, Commentable, Eventable
end

#L20, I would definitely use a exclamation mark, def trash!

@flockonus, having a bang method without an equivalent (non mutating) non bang method is not in line with ruby conventions.

@samuelkadolph Thanks for the heads up! I use bangs on every method that make permanent changes, as I read it was assumed the best practice.

We've been successful using concerns on our models. Each type of MoneyTransfer includes a mix of concerns: approvable, claimable, require funding sources to be verified, can accept credit cards and so on. Great pattern!

@flockonus I agree.

The ruby convention is that bang methods perform changes on the object itself (array.sort vs array.sort!).

This is not the case for active record models though. #save, #update_attributes will perform permanent changes and return false on failure. #save!, #update_attributes! will raise an exception on failure.

When I put a bang method on an activerecord model, I expect it to raise on failure.

@pcreux your approach is more coherent, thanks

@sandal covered bang methods in Ruby Best Practices on page 51.

Anyone have a recommendation for where to read about concerns? Nothing really stands out when searching [ruby concerns].

@jjb - search on ActiveSupport::Concern, there are a few discussions about Concerns. This is less ruby, more rails...

Great, thanks!

@jjb I recently wrote about organising your models using ActiveSupport::Concern on our company blog. Might be of use to you.

I like it, maybe app/controllers/concerns and app/models/concerns should be created and added to the load path by default

@csmuc, no need, just create app/concerns and it will autoloaded automatically.

clear and clean !

@dhh I don't think this works from the scoping standpoint:

Message.scoped.to_sql => "SELECT messages.* FROM messages WHERE messages.trashed = 0" # which is expected

but:

Message.trashed.to_sql => "SELECT messages.* FROM messages WHERE messages.trashed = 0 AND messages.trashed = 1" # which returns nothing b/c a message cannot be both T and F

Would you have to unscope from the default_scope in the trashed scope to get the expected behavior?

(Note: Rails 3.0.7)

Just FYI, having a similar conversation here: https://gist.github.com/979005#comments

Proposed solution: https://gist.github.com/1114452 <= thoughts? Thanks to @webficient for their input on it

I was wondering if is there any way to disable default_scope for a belongs_to relation, so I was trying to do that, overriding the getter method for the association, I've put all those things in a module.

# lib/unscopable.rb
module Unscopable

  def self.included(base)  
    base.extend ClassMethods
  end

  module ClassMethods

    def unscopable(*args, &block)
      args.each do |name|
        define_unscoped_method name
        alias_method_chain name, :unscoped
      end

    end  

    def define_unscoped_method(name)
      name = name.to_s
      model = name.classify
      attribute = "#{name}_id" 
      method_name = "#{name}_with_unscoped"

      define_method(method_name) do
        object = Object.const_get(model)
        object.unscoped { object.find eval(attribute) }
      end

    end

  end  
end

And for example we could disable the default scope in this way

# app/models/author.rb
class Author < ActiveRecord::Base
  include Unscopable
  belongs_to message
  unscopable :message
end

I don't really like the name 'unscopable' :-), I've used it just for this example.

what do you think?

Instead of the autoload paths:

  #{config.root}/app/controllers/concerns
  #{config.root}/app/models/concerns

Does anybody have any reasons (stylistic or practical) as to why not use

# app/models/concerns/trashable.rb
module Concerns::Trashable

Maybe it looks better than having to repeat the module for each concern?

  include Concerns::Trashable, Concerns::Subscribable, Concerns::Commentable, Concerns::Eventable

I think the given that Rails4 will have those paths autoloaded, it makes more sense to not namespace those paths, and thus not use Concerns::.

@justin808
I think you can create a folder called shared for all shared modules
So that you have to include Shared::Trashable in app/models/concerns/shared/trashable.rb, not just Trashable
Also if you got both Message::Trashable and Shared::Trashable, you won't be confused on which to include

I recently saw this article as a counterpoint to this approach to breaking down model classes:

http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/

Great, I also found few more ways to keep your code modular - http://amolnpujari.wordpress.com/2013/04/09/184/

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.