Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
Use concerns to keep your models manageable
# 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
@flockonus

This comment has been minimized.

Show comment Hide comment
@flockonus

flockonus Jun 8, 2011

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

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

@samuelkadolph

This comment has been minimized.

Show comment Hide comment
@samuelkadolph

samuelkadolph Jun 8, 2011

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

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

@flockonus

This comment has been minimized.

Show comment Hide comment
@flockonus

flockonus Jun 8, 2011

@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.

@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.

@pcreux

This comment has been minimized.

Show comment Hide comment
@pcreux

pcreux Jun 8, 2011

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!

pcreux commented Jun 8, 2011

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!

@pcreux

This comment has been minimized.

Show comment Hide comment
@pcreux

pcreux Jun 8, 2011

@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 commented Jun 8, 2011

@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.

@flockonus

This comment has been minimized.

Show comment Hide comment
@flockonus

flockonus Jun 8, 2011

@pcreux your approach is more coherent, thanks

@pcreux your approach is more coherent, thanks

@tjsingleton

This comment has been minimized.

Show comment Hide comment
@tjsingleton

tjsingleton Jun 8, 2011

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

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

@jjb

This comment has been minimized.

Show comment Hide comment
@jjb

jjb Jun 8, 2011

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

jjb commented Jun 8, 2011

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

@swards

This comment has been minimized.

Show comment Hide comment
@swards

swards Jun 8, 2011

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

swards commented Jun 8, 2011

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

@jjb

This comment has been minimized.

Show comment Hide comment
@jjb

jjb Jun 8, 2011

Great, thanks!

jjb commented Jun 8, 2011

Great, thanks!

@joshnesbitt

This comment has been minimized.

Show comment Hide comment
@joshnesbitt

joshnesbitt Jun 8, 2011

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

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

@csmuc

This comment has been minimized.

Show comment Hide comment
@csmuc

csmuc Jun 9, 2011

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

csmuc commented Jun 9, 2011

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

@samuelkadolph

This comment has been minimized.

Show comment Hide comment
@samuelkadolph

samuelkadolph Jun 9, 2011

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

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

@luckydev

This comment has been minimized.

Show comment Hide comment
@luckydev

luckydev Jun 9, 2011

clear and clean !

luckydev commented Jun 9, 2011

clear and clean !

@jeffreyiacono

This comment has been minimized.

Show comment Hide comment
@jeffreyiacono

jeffreyiacono Jul 29, 2011

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

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

@jeffreyiacono

This comment has been minimized.

Show comment Hide comment
@jeffreyiacono

jeffreyiacono Jul 29, 2011

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

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

@joselo

This comment has been minimized.

Show comment Hide comment
@joselo

joselo Nov 25, 2011

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?

joselo commented Nov 25, 2011

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?

@justin808

This comment has been minimized.

Show comment Hide comment
@justin808

justin808 Feb 5, 2013

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

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

@PikachuEXE

This comment has been minimized.

Show comment Hide comment
@PikachuEXE

PikachuEXE Apr 3, 2013

@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

@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

@kevinzen

This comment has been minimized.

Show comment Hide comment
@kevinzen

kevinzen May 23, 2013

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/

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/

@amolpujari

This comment has been minimized.

Show comment Hide comment
@amolpujari

amolpujari Mar 20, 2014

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

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

@izbor

This comment has been minimized.

Show comment Hide comment
@izbor

izbor Oct 27, 2014

concerns sucks compared to DCI. I hope ruby community will improve ruby to support DCI (extend/unextend modules on instances). Roles >>> concerns. Because roles are limited to contexts and you can't write spaghetti code, when you use method from role that should not be involved in current context. It is clear signal to developer - that something went wrong, architecture should be improved. In case of concerns you put everything in one object and let mess and spaghetti code happen in contexts (in Rails case in controllers or other "services" modules used by controllers).

izbor commented Oct 27, 2014

concerns sucks compared to DCI. I hope ruby community will improve ruby to support DCI (extend/unextend modules on instances). Roles >>> concerns. Because roles are limited to contexts and you can't write spaghetti code, when you use method from role that should not be involved in current context. It is clear signal to developer - that something went wrong, architecture should be improved. In case of concerns you put everything in one object and let mess and spaghetti code happen in contexts (in Rails case in controllers or other "services" modules used by controllers).

@izbor

This comment has been minimized.

Show comment Hide comment
@QuotableWater7

This comment has been minimized.

Show comment Hide comment
@QuotableWater7

QuotableWater7 Apr 16, 2015

@izbor besides the fact that this discussion has nothing to do with DCI, DCI is slow and is not really spreading a ton because of that.

@izbor besides the fact that this discussion has nothing to do with DCI, DCI is slow and is not really spreading a ton because of that.

@Bartuz

This comment has been minimized.

Show comment Hide comment
@Bartuz

Bartuz Oct 24, 2015

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment