Usando modelos ActiveRecord entre projetos diferentes


Leia em 5 minutos

Lego blocks Estou transformando o Presentta, que hoje nada mais é que um hack com um monte de tarefas que precisam ser feitas manualmente, em um produto. Decidi começar este novo projeto do zero, então decidi fazer algo diferente.

Em vez de ter um aplicativo monolítico, responsável por todas as partes do sistema, resolvi quebrar este sistema em diversos sistemas menores, cada um com sua responsabilidade. No Presentta, por exemplo, já tenho a parte de realtime com Node.js, interface web com Rails e API com Goliath, mas devem entrar outros sistemas mais para a frente.

Uma das coisas que pensei em fazer foi compartilhar modelos entre os sistemas Ruby. Quando eu ainda estava na WebCo, tínhamos tentado fazer algo semelhante usando submódulos do Git, sem muito sucesso. Lembre-se que naquela época não existia o Bundler. E foi justamente o Bundler que permitiu fazer esse compartilhamento de forma muito simples, como você pode conferir abaixo.

Criando o repositório de modelos

O primeiro passo foi definir o repositório de modelos que seria compartilhado entre os sistemas. No Bundler, todos os projetos precisam ter um gemspec, que é utilizado para empacotar uma biblioteca como RubyGem. No meu projeto, criei um arquivo chamado presentta-models.gemspec que tem o seguinte conteúdo.

# -*- encoding: utf-8 -*-
$:.push File.expand_path("../lib", __FILE__)
require "presentta/models/version"

Gem::Specification.new do |s|
  s.name        = "presentta-models"
  s.version     = Presentta::Models::Version::STRING
  s.platform    = Gem::Platform::RUBY
  s.authors     = ["Nando Vieira"]
  s.email       = ["fnando.vieira@gmail.com"]
  s.homepage    = "http://rubygems.org/gems/presentta-models"
  s.summary     = "Share ActiveRecord models between Presentta applications."
  s.description = s.summary

  s.files         = `git ls-files`.split("\n")
  s.test_files    = `git ls-files -- {test,spec,features}/*`.split("\n")
  s.executables   = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
  s.require_paths = ["lib"]

  s.add_dependency "mysql2", "~> 0.3"
  s.add_dependency "activerecord", "~> 3.2"
  s.add_dependency "simple_auth", "~> 1.4"
  s.add_dependency "validators", "~> 1.0"
  s.add_dependency "permalink", "~> 1.2"
  s.add_dependency "defaults", "~> 0.1"

  s.add_development_dependency "rake", "~> 0.9"
  s.add_development_dependency "rspec", "~> 2.8"
  s.add_development_dependency "factory_girl"
  s.add_development_dependency "factory_girl-preload"
end

Esse arquivo contém todas as dependências, como plugins do ActiveRecord e bibliotecas que serão usadas nos testes dos modelos.

Você também vai precisar definir o seu Gemfile. Neste caso, nós iremos utilizar as mesmas dependências definidas no arquivo gemspec.

source :rubygems
gemspec

O arquivo que irá iniciar a biblioteca é o lib/presentta/models.rb. Nele, além de carregar todas as gems que vou usar, ainda defino as configurações do ActiveRecord. Abaixo você pode conferir apenas as partes importantes.

require "mysql2"
require "active_record"

require "presentta/models/user"
require "presentta/models/courses"
require "presentta/models/your_model_here"

ActiveRecord::Base.timestamped_migrations = false
ActiveRecord::Base.attr_accessible(nil)
ActiveRecord::Migrator.migrations_paths = File.expand_path("../models/migrations", __FILE__)

module Presentta
  module Models
    def self.connect_with(file, environment)
      fail "#{file.inspect} should be a configuration file" unless file && File.exist?(file)
      ActiveRecord::Base.configurations = YAML.load_file(file)
      ActiveRecord::Base.establish_connection(environment)
    end
  end
end

Como será preciso estabelecer a conexão de diferentes lugares, preferi centralizar esta conexão à partir do método Presentta::Models.connect_with. Com isso já é possível configurar, por exemplo, as rake tasks que irão executar as migrações.

Uma das premissas do repositório de modelos era ter um modo de executar as migrations. Para fazer isso, criei um diretório lib/presentta/models/migrations, que tem todas as migrações, como 001_create_users.rb. Perceba que não estou usando o timestamp; como não vou usar os geradores do Rails, é mais prático escrever 001_create_users.rb que 20120306135200_create_users.rb, alteração realizada quando utilizei a configuração ActiveRecord::Base.timestamped_migrations = false.

O arquivo lib/presentta/models/rake.rb será carregado tanto pelo arquivo Rakefile do repositório de modelos, quando pelo aplicativo Rails.

ENV["RAILS_ENV"] ||= "development"

require "presentta/models"
require "rails"

Presentta::Models.connect_with(ENV["DATABASE_CONFIG"], Rails.env)
require "active_record/railtie"
load "active_record/railties/databases.rake"

task(:environment) {}

namespace :db do
  task(:load_config).clear_prerequisites.clear_actions
end

Duas coisas que você deve notar aqui. Para poder usar as rake tasks de fora do Rails, precisei definir a tarefa environment para um bloco que não faz nada. No Rails, ela existe e é responsável por inicializar o ambiente. Outra coisa importante é que removi a tarefa db:load_config que, basicamente, define o diretório de migrações e o arquivo de configuração. Mas atenção: ao sobrescrever esta tarefa, as migrações de Rails engines não serão detectadas e, por este motivo, devem ser copiadas para o diretório lib/presentta/models/migrations. No meu caso não uso nenhuma engine, então isso não é um problema.

Agora, no arquivo Rakefile da raíz do repositório de modelos, é preciso carregar as rake tasks do banco de dados. Como uso RSpec, também já coloquei sua tarefa neste arquivo.

require "rspec/core/rake_task"
RSpec::Core::RakeTask.new

ENV["DATABASE_CONFIG"] = File.expand_path("../database.yml", __FILE__)
require "presentta/models/rake"

O arquivo de configuração do ActiveRecord é lido à partir da variável de ambiente DATABASE_CONFIG, que aqui definimos para um arquivo na raíz do repositório. Nada diferente do que você já está acostumado a ver no Rails.

defaults: &DEFAULTS
  adapter: mysql2
  encoding: utf8
  reconnect: true
  pool: 5
  host: localhost
  username: root
  password:

development:
  <<: *DEFAULTS
  database: presentta_development

test:
  <<: *DEFAULTS
  database: presentta_test

Para listar todas as tarefas disponíveis, execute o comando rake -T db.

$ rake -T db
rake db:create          # Create the database from config/database.yml for the current Rails.en...
rake db:drop            # Drops the database for the current Rails.env (use db:drop:all to drop...
rake db:fixtures:load   # Load fixtures into the current environment's database.
rake db:migrate         # Migrate the database (options: VERSION=x, VERBOSE=false).
rake db:migrate:status  # Display status of migrations
rake db:rollback        # Rolls the schema back to the previous version (specify steps w/ STEP=n).
rake db:schema:dump     # Create a db/schema.rb file that can be portably used against any DB s...
rake db:schema:load     # Load a schema.rb file into the database
rake db:seed            # Load the seed data from db/seeds.rb
rake db:setup           # Create the database, load the schema, and initialize with the seed da...
rake db:structure:dump  # Dump the database structure to db/structure.sql. Specify another file...
rake db:version         # Retrieves the current schema version number

Se tudo estiver configurando de forma correta, você conseguirá ver qual a versão do seu banco de dados executando o comando rake db:version.

$ rake db:version
Current version: 7

Do lado do repositório de modelos, você não precisa fazer mais nada!

Configurando o Rails

Agora você precisa configurar sua aplicação Rails para utilizar o repositório de modelos. O primeiro passo é configurar o arquivo Gemfile. Idealmente, seria interessante apontar os ambientes de desenvolvimento e teste para o repositório local, e o ambiente de produção para o repositório do Codeplane. Mas isso não é possível no Bundler, embora seja uma funcionalidade prevista para o release 1.1 (estou usando o Bundler 1.1 RC7 e ainda não é possível). Como ainda estou na fase de desenvolvimento, vou deixar para me preocupar com isso quando chegar a hora. Por enquanto, meu arquivo Gemfile é algo como isto:

source :rubygems

gem "rails"                   , "3.2.2"
gem "responders"              , "~> 0.8"
gem "simple_form"             , "~> 2.0"
gem "simple_presenter"        , "~> 0.1"
gem "swiss_knife"             , "~> 1.0"

group :development, :test do
  gem "presentta-models"      , :path => "~/Sites/presentta-models"
  gem "thin"                  , "~> 1.3"
  gem "awesome_print"         , :require => false
  gem "pry"                   , :require => false
  gem "rspec-rails"           , "~> 2.7"
end

group :test do
  gem "factory_girl"          , "~> 2.2"
  gem "factory_girl-preload"  , "~> 1.0"
  gem "test_notifier"         , "~> 0.4", :require => "test_notifier/runner/rspec"
  gem "spork"                 , "~> 0.9.0"
  gem "capybara"              , "~> 1.1"
end

O seu arquivo config/application.rb também precisará ser modificado. A parte importante aqui são os arquivos que o Rails irá carregar ou, melhor dizendo, não irá carregar, como o arquivo active_record/railtie.

require File.expand_path("../boot", __FILE__)

require "rails"
require "action_controller/railtie"
require "action_mailer/railtie"
require "active_resource/railtie"

Bundler.require(:default, Rails.env) if defined?(Bundler)

module Presentta
  class Application < Rails::Application
    config.time_zone = "Brasilia"
    config.i18n.load_path += Dir[Rails.root.join("config/locales/**/*.yml").to_s]
    config.i18n.default_locale = :"pt-BR"
    config.encoding = "utf-8"
    config.filter_parameters += [:password]
    config.assets.enabled = false
    config.generators.assets = false
    config.generators.test_framework :rspec, :fixtures => false, :view_specs => false
  end
end

Para estabelecer a conexão com o banco de dados, nós precisaremos utilizar o método Presentta::Models.connect_with. Crie um arquivo config/initializers/activerecord.rb com o conteúdo à seguir.

Presentta::Models.connect_with(
  Rails.root.join("config/database.yml").to_s,
  Rails.env
)

require "active_record/railtie"

Finalmente, é preciso carregar as rake tasks. Crie o arquivo lib/tasks/db.rake.

ENV["DATABASE_CONFIG"] = File.expand_path("../../../config/database.yml", __FILE__)
require "presentta/models/rake"

Assim como no repositório de modelos, você pode ver as rake tasks disponíveis com o comando rake -T db.

Configurando outros apps

Seus outros aplicativos como Goliath e Sinatra também pode usar os modelos. Para isso, além de definir o repositório de modelos no Gemfile, você também precisará fazer a conexão usando as linhas à seguir.

require "presentta-models"

Presentta::Models.connect_with(
  File.expand_path("../database.yml", __FILE__),
  ENV["RACK_ENV"]
)