0

I am building a new app in Rails for an internal project that changes slightly each year based on the requirements of our clients. Any changes between years will occur within the models (add/remove columns, formatting, reports, etc). My plan is to build it to the requirements for this year and going forward each year I will create a new model and migration (e.g. Sample2019Record, Sample2020Record) that will encapsulate the requirements for that year. The app also needs to render previous year data and all the data is scoped based on the year meaning there is no need to render or query multiple years data. I would prefer not to create a new app each year since that is more apps that need to be maintained.

So my idea is to include the year into the URL (/2018/sample/new or /sample/new?year=2018) and parse the model based on the year ("Sample#{year}Record"). Can rails handle this safely and is there a Gem that can help assist with this approach?


Here is what I came up with, thanks for the advice.

routes.rb

get '/:year/samples', to: 'samples#index', as: :samples, defaults: { year: Time.current.year }

Routes will always default to the current year

application_controller.rb

before_action :check_year

def check_year
  if params.has_key?(:year)
    if "Sample#{params[:year]}Record".safe_constantize.nil?
      redirect_to root_path(year: Time.current.year), notice: "Invalid Year"
    end
  else
    redirect_to root_path(year: Time.current.year), notice: "Invalid Year"
  end
end

def get_sample_record(year=Time.current.year)
  "Sample#{year}Record".safe_constantize
end

Added a before_action to check the year parameter and added the get_sample_record method to safely constantize the record that can be called from any controller with an optional year like so:

sample_controller.rb

sample_2018_record = get_sample_record
sample_2018_record.count
#> 304
sample_2017_record = get_sample_record 2017
sample_2017_record.count
#> 575683

The result will be nil if an invalid year is passed so I will handle the check in the controller.

Booshwa
  • 109
  • 2
  • 9
  • Rails can handle anything you can code. I'd be wary of creating entirely new classes, but without much context it's difficult to know what direction makes the most sense. *Can* Ruby/Rails handle this? Absolutely, but if you're keeping the data in the same table, woe will likely ensue. – Dave Newton Feb 14 '18 at 15:48
  • Thanks for the quick reply! My plan is to keep separate models and migrations for each year. I am currently planning on concatenating the model name with the year and constantize the model similar to this: https://stackoverflow.com/questions/14070369/how-to-instantiate-class-from-name-string-in-rails. I'm pretty new to Rails and can't find any Gems or help to support this approach and wondering if there is a better way to do this. – Booshwa Feb 14 '18 at 16:36
  • The code itself would be relatively straight-forward. The issues (may) come when you're interacting w/ a single table and multiple models. If it's separate tables then there's no issue, it's just a little funky. Nothing a before filter wouldn't handle easily. – Dave Newton Feb 14 '18 at 17:41

1 Answers1

1

As @DaveNewton said, this seems like it should work fine so long as you keep data corresponding to different years' requirements in different tables. A few other observations:

Rails has a helper method constantize for parsing a model from a string:

klass_name = "Sample#{year}Record"
record = klass_name.constantize.new()

will make the variable record an instance of your class corresponding to the year variable. You may find it helpful to use a Factory pattern to encapsulate the process.

Also. be careful how you name and organise your files. You may find this thread helpful when working with the Rails infectors for classes with numbers in their names. A big part of working with Rails is allowing its magic to work for you rather than unwittingly trying to work against it.

As a general rather than Rails-specific piece of advice, I'd also give a considerable amount of thought to how you could define a common public interface for records that will persist across years. A codebase featuring things like

if record.instance_of? Sample2018Record
  record.my_2018_method
elsif record.instance_of? Sample2019Record
  record.my_method_only_relevant_to_2019
...

will become very difficult to reason about, especially for developers who join after a couple of years. Ruby has extremely powerful tools to help you duck type very effectively.

rwold
  • 2,216
  • 1
  • 14
  • 22