Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to extend ActiveRecord::Migration with additional methods?

I'm creating a Ruby gem and would like to extend ActiveRecord::Migration with my own helpers for creating the necessary columns. (This is similar to what Devise does when creating migrations for their various authentication strategies.) I realize the functionality I'm adding is pretty trivial itself and there are probably better/more efficient ways of doing this - I'm attempting this as a learning experience rather than as something with practical application. I just want to understand how to do something as invasive as adding new migration capabilities in Rails.

What I have so far builds into a gem successfully and installs, but when I attempt to run a migration like:

class CreatePosts < ActiveRecord::Migration
  def self.up
    create_table :posts do |t|
      t.string :name
      t.string :title
      t.text :content
      t.hideable
      t.tracks_hidden_at
      t.timestamps
    end
  end
end

... it fails saying that hideable isn't defined.

I've looked into the way that Devise has done this and I have to admit I'm a little lost, but I've tried to fumble through it. I've done the following:

Extended ActiveRecord with my new model additions and created a method to apply the schema changes based on my new migration methods

require 'orm_adapter/adapters/active_record'

module HiddenRecord
  module Orm
    # This module contains some helpers and handle schema (migrations):
    #
    #   create_table :accounts do |t|
    #     t.hideable
    #     t.tracks_hidden_timestamp
    #   end
    #
    module ActiveRecord
      module Schema
        include HiddenRecord::Schema

        # Tell how to apply schema methods.
        def apply_hiddenrecord_schema(name, type, options={})
          column name, type.to_s.downcase.to_sym, options
        end
      end
    end
  end
end
ActiveRecord::Base.extend HiddenRecord::Models
ActiveRecord::ConnectionAdapters::Table.send :include, HiddenRecord::Orm::ActiveRecord::Schema
ActiveRecord::ConnectionAdapters::TableDefinition.send :include, HiddenRecord::Orm::ActiveRecord::Schema

Created a Schema module similar to Devise's schema.rb that defines the methods I want to use in the migration and calls a method to apply the schema

module HiddenRecord
  # Holds schema definition for hiddenrecord model options.
  module Schema
    # Sets the model as having hidable rows
    #
    # == Options
    # * :null - When true, allows the hidden row flag to be null
    # * :default - Used to set default hidden status to true. If not set, default is false (rows are not hidden)
    def hideable(options={})
      null = options[:null] || false
      default = options[:default] || false

      apply_hiddenrecord_schema :hiddenrecord_is_row_hidden, Boolean, :null => null, :default => default
    end

    # Sets the model to record the timestamp when a row was hidden
    def tracks_hidden_timestamp()
      apply_hiddenrecord_schema :hiddenrecord_hidden_at, DateTime
    end
  end
end

Added methods for the models to support the new fields

module HiddenRecord
  module Models
    # This module implements the hideable API
    module Hideable
      def self.included(base)
        base.class_eval do
          extend ClassMethods
        end
      end

      scope :visible, where(:hiddenrecord_is_row_hidden => true)

      def hidden?
        return hiddenrecord_is_row_hidden || false
      end

      def hide
        hiddenrecord_is_row_hidden = true
      end

      def hide!
        hiddenrecord_is_row_hidden = true
        save!
      end

      def unhide
        hiddenrecord_is_row_hidden = false
      end

      def unhide!
        hiddenrecord_is_row_hidden = false
        save!
      end

    end
  end
end

Load the schema and model files and in the main module of the gem

module HiddenRecord
  autoload :Schema, 'hiddenrecord/schema'
  autoload :Models, 'hiddenrecord/models'
  ...
end
require 'hiddenrecord/models/hideable'
require 'hiddenrecord/models/tracks_hidden_timestamp'

Again, recognizing that this is primarily a learning experience, I'm hoping someone can point me in the right direction on how to do this. I'm attempting this on Rails 3.

like image 326
Chris Hart Avatar asked Nov 21 '10 20:11

Chris Hart


People also ask

How does Rails know which migration to run?

Rails uses this timestamp to determine which migration should be run and in what order, so if you're copying a migration from another application or generate a file yourself, be aware of its position in the order. This generator can do much more than append a timestamp to the file name.

Which command is used to rollback migration in Rails?

To undo a rails generate command, run a rails destroy command. You can then edit the file and run rake db:migrate again. (See how to roll back a Migration file to rollback a specific migration or multiple migrations.)

How does Rails migration work internally?

A Rails migration is a tool for changing an application's database schema. Instead of managing SQL scripts, you define database changes in a domain-specific language (DSL). The code is database-independent, so you can easily move your app to a new platform.


2 Answers

Here's how I added custom migration fields with Rails 2 and MySQL for a previous project. Works great.

I don't know how much of this applies to your exact need, so feel free to ask me questions.

I put this code in Rails.root/lib/dbd_migration_helper.rb

module Ddb

  module MigrationHelper

    def self.included(base) # :nodoc:
      base.send(:include, InstanceMethods)
    end

    module InstanceMethods
      def active    (column_name=:active)     column(column_name, :boolean, :default=>true) end
      def email     (column_name=:email)      column(column_name, :string)     end
      def latitude  (column_name=:latitude)   column(column_name, :float)      end
      def longitude (column_name=:longitude)  column(column_name, :float)      end
      def position  (column_name=:position)   column(column_name, :integer)    end
    end
  end
end

require 'activerecord'
if defined?(ActiveRecord::ConnectionAdapters::TableDefinition)
   ActiveRecord::ConnectionAdapters::TableDefinition.send(:include, Ddb::MigrationHelper)
end
like image 130
joelparkerhenderson Avatar answered Oct 18 '22 19:10

joelparkerhenderson


Quick note, these lines:

ActiveRecord::ConnectionAdapters::Table.send :include, HiddenRecord::Orm::ActiveRecord::Schema
ActiveRecord::ConnectionAdapters::TableDefinition.send :include, HiddenRecord::Orm::ActiveRecord::Schema

don't appear to be include'ing the correct module. I think they should be:

ActiveRecord::ConnectionAdapters::Table.send :include, HiddenRecord::Schema
ActiveRecord::ConnectionAdapters::TableDefinition.send :include, HiddenRecord::Schema

but then, you don't appear to have #tracks_hidden_at defined anywhere either.

like image 44
Michael Slade Avatar answered Oct 18 '22 18:10

Michael Slade