I'm in the process of writing an Importable concern for my rails project. This concern will provide a generic way for me to import a csv file into any model that includes Importable.
I need a way for each model to specify which field the import code should use to find existing records. Are there any recommended ways of adding this type of configuring for a concern?
It is a bit of Rails carbohydrates sprinkled upon a Ruby module. What ActiveSupport::Concern does for you is it allows you to put code that you want evaluated inside the included block. For example, you want to extract the trashing logic out of your model.
A Rails Model is a Ruby class that can add database records (think of whole rows in an Excel table), find particular data you're looking for, update that data, or remove data. These common operations are referred to by the acronym CRUD--Create, Remove, Update, Destroy.
Using extend ActiveSupport::Concern tells Rails that we are creating a concern. The code within the included block will be executed wherever the module is included. This is best for including third party functionality. In this case, we will get an error if the before_action is written outside of the included block.
A slightly more "vanilla-looking" solution, we do this (coincidentally, for the exactly some csv import issue) to avoid the need for passing arguments to the Concern. I am sure there are pros and cons to the error-raising abstract method, but it keeps all the code in the app folder and the models where you expect to find it.
In the "concern" module, just the basics:
module CsvImportable
extend ActiveSupport::Concern
# concern methods, perhaps one that calls
# some_method_that_differs_by_target_class() ...
def some_method_that_differs_by_target_class()
raise 'you must implement this in the target class'
end
end
And in the model having the concern:
class Exemption < ActiveRecord::Base
include CsvImportable
# ...
private
def some_method_that_differs_by_target_class
# real implementation here
end
end
Rather than including the concern in each model, I'd suggest creating an ActiveRecord
submodule and extend ActiveRecord::Base
with it, and then add a method in that submodule (say include_importable
) that does the including. You can then pass the field name as an argument to that method, and in the method define an instance variable and accessor (say for example importable_field
) to save the field name for reference in your Importable
class and instance methods.
So something like this:
module Importable
extend ActiveSupport::Concern
module ActiveRecord
def include_importable(field_name)
# create a reader on the class to access the field name
class << self; attr_reader :importable_field; end
@importable_field = field_name.to_s
include Importable
# do any other setup
end
end
module ClassMethods
# reference field name as self.importable_field
end
module InstanceMethods
# reference field name as self.class.importable_field
end
end
You'll then need to extend ActiveRecord
with this module, say by putting this line in an initializer (config/initializers/active_record.rb
):
ActiveRecord::Base.extend(Importable::ActiveRecord)
(If the concern is in your config.autoload_paths
then you shouldn't need to require it here, see the comments below.)
Then in your models, you would include Importable
like this:
class MyModel
include_importable 'some_field'
end
And the imported_field
reader will return the name of the field:
MyModel.imported_field
#=> 'some_field'
In your InstanceMethods
, you can then set the value of the imported field in your instance methods by passing the name of the field to write_attribute
, and get the value using read_attribute
:
m = MyModel.new
m.write_attribute(m.class.imported_field, "some value")
m.some_field
#=> "some value"
m.read_attribute(m.class.importable_field)
#=> "some value"
Hope that helps. This is just my personal take on this, though, there are other ways to do it (and I'd be interested to hear about them too).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With