seejohnrun / proxy_record

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

ProxyRecord

Motivation

ActiveRecord is a tremendously powerful and popular ORM for Rails, but some of the access patterns that result from using it can lead to a difficulty for teams in larger codebases to define clear public interfaces for their models.

For example, when defining User as a subclass of ActiveRecord::Base, external methods like .destroy_all, .update_all are exposed to the entire application. We're left unable to create clear contracts for our models, and as a result we see a bunch of code in views, controllers, and other models reaching directly into methods which aren't intended to be used. Some particular common cases:

  • Views and controllers calling where(...) on an association to further scope it, instead of utilizing appropriate named scopes or model methods.

  • Public interfaces for classes are made larger by default, so rather than having to test the correct path, we test all of them (or add linters) for fear that new developers might use the wrong path for an operation.

  • When certain fields need additional behavior on modification, we can't tightly control the path to updates so we end up having to rely on hooks like after_create to do additional processing.

The way that ActiveRecord works is pretty desirable for a smaller application, but as the project & teams grow, the more we fall into usage patterns which can make things harder to maintain.

This library is a proof-of-concept of what it might look like to introduce a layer in front of the data model, giving developers tighter control over how their objects are used.

Usage

To create a model, instead of subclassing directly from ApplicationRecord, you should subclass from ProxyRecord[ApplicationRecord]. Additional validations and associations can be defined inside of a data_model_eval block like this:

class User < ProxyRecord[ApplicationRecord]
  data_model_eval do
    validates :name, presence: true
    has_many :posts, foreign_key: 'user_id', class_name: 'Post::DataModel'
  end
end

This new class, User doesn't come with any methods by default, which means when you want to provide access to underlying data model methods, you need to create them yourself. You can access the underlying data model via the data_model local.

class User < ProxyRecord[ApplicationRecord]
  # ...

  def display_name
    "#{data_model.name} (#{data_model.login})
  end
end

You can also define class methods, and access the underlying data model class via data_model_class like so:

class User < ProxyRecord[ApplicationRecord]
  # ...

  def self.recent_user_count
    data_model_class.where('accessed_at > ?', 1.hour.ago).count
  end
end

Wrapping objects

Sometimes you'll need to be able to return an ActiveRecord object, and in those cases you'll want to wrap with the appropriate ProxyRecord class. For that you can use ProxyRecord.wrap:

class User < ProxyRecord[ApplicationRecord]
  # ...

  def create_post(title:)
    post_data_model = data_model.posts.create(title: title)
    ProxyRecord.wrap(post_data_model) # Returns a `Post`
  end
end

Delegates

You'll often want to be able to create direct delegates for underlying methods, and to reduce the tedious nature of delegating and potentially wrapping the response, you can use class_proxy_delegate and instance_proxy_delegate to automatically wrap delegated methods:

class User < ProxyRecord[ApplicationRecord]
  # ..

  class_proxy_delegate :create

  instance_proxy_delegate :login, :posts
end

user = User.create # Returns a User, subclass of ProxyRecord::Proxy
user.login
user.posts # Returns a ProxyRecord::ProxyCollection[Post]

About


Languages

Language:Ruby 100.0%