Extend AR to return Monads?

Every time I use AR & dry-monads together, I keep thinking about how neat it would be if ActiveRecord finders & associations returned Maybes (on queries) or Results (on mutations). I had a quick look last night to satisfy my own curiosity, and found that find_by is doable:

module ActiveRecord
  module Core
    module ClassMethods
      alias_method :old_find_by, :find_by
      def find_by(*args)
        Maybe(old_find_by(*args))
      end
    end
  end
end

I imagine that anything beyond this would get hairy quickly, though. Has anyone tried?

It looks like a good idea, but it definitely not good path for various reasons:

I’d recommend to continue encapsulating your business objects on modules/commands/systems you can test by their own independent of the data abstraction/orm.

2 Likes

I agree with Esparta that monkey-patching AR is not a good idea. It is not a good idea in general.

You can add a simple wrapper around your models that would delegate to the underlying methods and wrap results with Monads. I also recommend defining your own data-access method and treating AR “finders” and other query DSL methods as “private” from the application point of view.

I don’t feel I was very clear, sorry :slight_smile: I’m in complete agreement that monkey-patching is almost never worth it.

I was thinking more along the lines of a mixed-in module that detects the finders and associations that Rails already declared, and wraps them similar to Monads::Do.

Speaking as someone who actually did monkeypatch ActiveRecord, I also agree with Esparta’s advice here: don’t. I don’t regret doing it, because I knew what I was getting myself into; but if you look at the sourcecode you will see the lengths I had to go to.

My approach to ActiveRecord actually veers far to the opposite direction: you should treat ActiveRecord as an entirely private API. I make the model constants private and make my own public APIs to query data.

Here’s a quick example of what I mean

module Certificates
  class Record < ApplicationRecord
    self.table_name = "certificates"
  end
  private_constant :Record

  def self.find_by_name(cn)
    M::Maybe(Record.find_by(cname: cn))
  end
end

Why go to this trouble? Have you ever looked at the public interface of an ActiveRecord model? The API is huge. Most people give up and just integration-test everything with factories or fixtures. But the downside to that is that your test suite becomes intolerably slow.

I wrote a greenfield API service with this method, and was able to hit 100% test coverage with miniscule amounts of factory tests; the vast majority simply stubs out the interfaces under test. Only the stuff that literally talks to the DB needs to be integration tested this way.

Perhaps it’s a little scorched-earth, but AR is such a complex interface that I feel like it will become the center of gravity if you allow it. A data mapper pattern is better, but if you must use AR this is what I recommend.

2 Likes

Thanks, Adam.

I’m doing something similar to that on my current project; more like a traditional repository pattern. The idea of an embedded Record class is a new one to me. Very interesting, thank you :blush: