Search code examples
ruby-on-railsruby-on-rails-4activerecordroutesconstraints

Rails Active Record / Database Data in a Route Constraint


So I've recently been tidying up a lot of route constraints in my Rails apps by adding some Constraint classes. Mostly this has been nicely modularized, dry, and intelligible, so Ill probably be sticking with this pattern for future development.

I am wondering if it is a "bad practice" to query for Active Record data from within a Constraint class. For example, consider the following class:

class CategoryConstraint
  include DisplayConstraintHelper

  def initialize
    @names = Category.table_exists? ? Category.pluck(:name) : []
  end

  def matches?(request)    
    @names.include?(request.path_parameters[:category]) && display_matches?(request)
  end
end

I discovered today that I need table_exists? in the initialize method above, as my routes will be evoked every time I run a rake task - such as setting up a development database. If the database doesnt yet exist, and the application is loaded at all, an error is thrown on the pluck call since the table doesnt exist yet.

While calling table_exists? might not be expensive, Im quite bothered that it would be called in production for every request ever, when there will be no chance the table wont exist after the application launches. This led me to wondering if it was a bad idea to begin with, using data from an Active Record table to create a route constraint.

I do want my route constrained to these values, though, and the values can change based on site content administration (so hard coding them is silly). I want users to get a 404 when they use an invalid category name, rather than my site load normally and say "sorry no items for category XYZ." Thus Im stuck with the problem of needing this table_exists? call, for the initial prod deploy, for new developers setting up and environment for the first time, and for anyone dropping and setting up a new database for the site.

Does this seem like a bad practice? Is it simply something I will have to live with, if I want the application to function as expected when the database isnt setup?

Does anyone know of a better alternative?


Solution

  • The problem is more likely that you're making database queries from the constructor (def initialize), which is called when routes are defined.

    Kadu's answer should work fine because his db queries happen within a lambda, which is only called when a request comes in.

    If you're ok with calling Category.all.pluck(:name) on every matching request, then you could have the same constraint class with the query in the matches? method:

    class CategoryConstraint
      include DisplayConstraintHelper
    
      def matches?(request)
        Category.pluck(:name).include?(request.path_parameters[:category]) && display_matches?(request)
      end
    end
    

    If you don't need all names in memory, you would be better served by:

    Category.exists?(name: request.path_parameters[:category])
    

    If instead your goal is to cache all names in memory so you're not making the query in every call, you can lazy assign it at the class level:

    class CategoryConstraint
      include DisplayConstraintHelper
    
      mattr_reader :names, instance_accessor: false
    
      def matches?(request)
        names.includes(request.path_parameters[:category]) && display_matches?(request)
      end
    
      def names
        self.class.names ||= Category.pluck(:name)
      end
    end
    

    As for whether having ActiveRecord queries is acceptable in a constraint - yes, I think it's totally acceptable and can be a good idea for things like admin-specific routes. Just be careful to only execute the queries from within the #matches? or #call methods so it happens within a request lifecycle.