Tag: ActiveModel

Sharing models between Rails apps – Keeping Rails engine migrations in the engine

Note 1: If you have an option to use micro services and/or event-sourcing, go for it! Rails solutions based on shared models and a single shared database, can bring you in a longer perspective more harm than good.

Note 2: This approach is great when your data is tightly coupled and you can't easily switch from a single app to a distributed model.

Sharing models between Rails apps - basics

The concept of shared models is really well described by Fabio Akita in following articles:

However, his approach towards migrations comes from his really specific use-case (two DBs in a single app - one shared and one private).

Managing migrations - standard Rails engine approach

Standard Rails engine approach assumes that your migrations will be copied from the engine into the application when you run following command:

bundle exec rake engine_name_engine:install:migrations

This is great when:

  • You have a single "master" application that you want to decompose with engines
  • you have multiple applications with separate databases and you want to use business logic from the engine from each of them
  • If you want to to have a single "master" application that is supposed to run all the migrations from the engine

However with some benefits, you get a huge (in my case) drawback - when you copy migrations, their timestamp is being changed. It means that if you share same database across multiple applications that also share the same engine, you will end up with a single migration being executed (assuming you install the migrations) from each of the separate applications.

Single database and no master application

This won't do if your case is similar to mine, that is:

  • Single database
  • Multiple applications that need to share same models and scopes
  • Migrations should be executed in the first application that is being deployed after the model engine change (not from the "master" app)
  • There should not be any patching / adding  code into any of the apps that will use shared models gem

Keeping your migrations inside your model engine

Solution for such a case (in which all the models are being kept inside the gem) is pretty simple: you just need to append migrations into your apps migrations path without:

  • Copying them from the model engine gem
  • Changing the timestamp
  • Executing the same migrations multiple times

To achieve such a behavior, we will take advantage of how Rails config paths, migrations and initializers work:

  • Config paths aren't bound to the Rails.root directory (which means that they can use files from gems and other locations)
  • Config paths are appendable (which means we can add our gem migrations into the app migration list without changing timestamps and copying files)
  • Engine initializer allow us to bind this process from the model gem, keeping the apps untouched (they will think that those migrations are theirs)
  • Rails migrations execution details are stored in schema_migrations table, so unless executed exactly the same moment (so transactions overlap) a single gem migration will not be executed twice

All of this comes down few lines of Ruby inside Rails engine engine class (engine_path/lib/engine_name/engine.rb):

initializer :append_migrations do |app|
  # This prevents migrations from being loaded twice from the inside of the
  # gem itself (dummy test app)
  if app.root.to_s !~ /#{root}/
    config.paths['db/migrate'].expanded.each do |migration_path|
      app.config.paths['db/migrate'] << migration_path
    end
  end
end

TL;DR - Final solution

engine_path/lib/engine_name/engine.rb:

module ModelEngine
  class Engine < ::Rails::Engine
    initializer :append_migrations do |app|
      # This prevents migrations from being loaded twice from the inside of the
      # gem itself (dummy test app)
      if app.root.to_s !~ /#{root}/
        config.paths['db/migrate'].expanded.each do |migration_path|
          app.config.paths['db/migrate'] << migration_path
        end
      end
    end
  end
end

Summary

Most of the time sharing models is bad, but there are some cases app data is really tightly coupled together and exposing API with building microservices around it would mean a huge overhead. For such cases model gem with internal migrations might be a great solution.

Warning: If you decide to go that road, please make sure, that:

  • Your models are stable
  • Your models are slim and without any business logic
  • Your models don't have any callbacks or external dependencies
  • If your models have external dependencies, make them model gem dependencies
  • Your models are loosely coupled (if you follow Akitas approach with concerns it won't be hard)
  • Your applications are well tested
  • Your model gem is well tested
  • You don't use model validations - instead you can use Reform, Dry-Validations or any other solution that allows you to move validations logic out of models
  • All the model related things and migrations are inside the model gem
  • Migrations from external gems like Devise or FriendlyId are also inside the gem

With all of this in mind, you should be fine :-)

Cover photo by: Unsplash on Creative Commons 0 license.

ActiveResource relations – a bit of magic to make it look and feel more like ActiveModel relations

ActiveResource collection new problem

ActiveResource can be pretty helpful when you have a RESTful JSON API. Although it has some limitations. One of the most irritating is a lack of nested resources new scope method. When you have structure like this:

class User < ActiveResource::Base
  self.site = 'your_api_end_point'

  has_many :daily_stats
end

class DailyStat  < ActiveResource::Base
  self.site = 'your_api_end_point'

  belongs_to :user

  schema do
    attribute 'videos_count', :integer
    attribute 'videos_excess', :integer
  end
end

You can do some basic stuff:

User.last #=> User instance
User.all #=> [User, User]
user = User.last
user.daily_stats #=> [DailyStat, DailyStat]

But unfortunately if you try something like this:

user = User.last
stat = user.daily_stats.new
stat.save!

You'll get following error:

user.daily_stats.new
NoMethodError: undefined method `new' for #<ActiveResource::Collection:0x000000080d1a30>

Of course we can do this the other way around:

user = User.last
stats = DailyStat.new(user_id: user.id)
stats.save!

But it just doesn't seem right (well at least after working with ActiveRecord). ActiveResource::Collection doesn't support building resources through it.

alias_method and a bit of magic as a solution

To obtain such a behaviour we have to:

  • save original relation method using alias_method
  • create a module that will contain our extension that will allow us to build new resources directly
  • define relation method that will mix the module with original relation method output
  • return mixed relation output

Once we have all of this, we will be able to just:

user = User.last
new_stat = user.daily_stats.new
new_stat.save!

overwriting method without losing the original one

Ok, so we have our relation daily_stats method that will return us a given ActiveResource::Collection. We will have to overwrite it, but we can't lose the original one. To obtain this, we can use alias_method:

class User < ActiveResource::Base
  self.site = 'your_api_end_point'

  has_many :daily_stats

  alias_method :native_daily_stats, :daily_stats

  def daily_stats
    # Now we can do whatever we want here, because we can get
    # the ActiveResource::Collection of DailyStat via
    # native_daily_stats method
    # Something fancy will happen here...
    # and after that we will just return native_daily_stats
    native_daily_stats
  end
end

It's worth pointing out, that you can use this trick to redefine/change method without losing the original one. Especially when you can't use super (because it's not an inherited one, etc).

Extension module for our new daily_stats

Now we have to create our extension that will be used to modify the ActiveResource::Collection (but only in a daily_stats scope):

class DailyStat < ActiveResource::Base
  module RelationExtensions
    def new(params = {})
      params.merge!(original_params)
      resource_class.new(params)
    end
  end

  # Here should go previously declarated DailyStat class code...
end

Hooking it all togethers

Finally, we can join all previously created elements in a new daily_stats method:

class User < ActiveResource::Base
  # User code...  

  def daily_stats
    original_daily_stats.extend DailyStat::RelationExtensions
    original_daily_stats
  end
end

Now you can create resources that belong to other, directly via their scope.

TLl;DR version

class User < ActiveResource::Base
  self.site = 'your_api_end_point'

  has_many :daily_stats

  alias_method :original_daily_stats, :daily_stats

  def daily_stats
    original_daily_stats.extend DailyStat::RelationExtensions
    original_daily_stats
  end
end

class DailyStat  < ActiveResource::Base
  module RelationExtensions
    def new(params = {})
      # Here magic happens - original params contain relation details (user_id: user.id)
      params.merge!(original_params)
      resource_class.new(params)
    end
  end

  self.site = 'your_api_end_point'

  belongs_to :user

  schema do
    attribute 'videos_count', :integer
    attribute 'videos_excess', :integer
  end
end

Copyright © 2024 Closer to Code

Theme by Anders NorenUp ↑