├── .gitignore ├── .travis.yml ├── Gemfile ├── README.md ├── Rakefile ├── class-table-inheritance.gemspec ├── init.rb ├── lib ├── class-table-inheritance.rb └── class-table-inheritance │ ├── class-table-inheritance.rb │ ├── inherits-migration.rb │ └── version.rb └── test ├── class_table_inheritance_test.rb ├── database.yml ├── models ├── book.rb ├── manager.rb ├── mod.rb ├── mod │ ├── user.rb │ └── video.rb └── product.rb ├── schema.rb └── test_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | /Gemfile.lock 3 | test/cti_plugin.sqlite3 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.7.1 4 | - 2.6.6 5 | - 2.5.8 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | # Specify your gem's dependencies in correios.gemspec 4 | gemspec -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/brunofrank/class-table-inheritance.svg?branch=master)](https://travis-ci.org/brunofrank/class-table-inheritance) 2 | 3 | Change log 4 | ---------- 5 | 6 | ### 1.5.0 7 | 8 | * Add ActiveRecord 6.0 support 9 | * Require Ruby 2.5 or newer. 10 | 11 | 12 | ### 1.4.0 13 | 14 | * Add ActiveRecord 5.2 support 15 | * Require Ruby 2.2 or newer. 16 | 17 | 18 | ### 1.3.1 19 | 20 | * Removed 'set_primary_key' deprecation warning 21 | * Make the gem depencies explicit and require ActiveRecord 4.x or 5.0 22 | 23 | 24 | ### 1.3.0 25 | * Now you can inherits from and to modules like inherits_from 'Module::Model', see the the name of 26 | field must be module_model_id:integer thanks for Marc Remolt (https://github.com/mremolt). 27 | * Unit test 28 | 29 | 30 | Note about version 31 | ------------------ 32 | 33 | If you are using Rails 2.3.8 or other version < 3, you have to use the version 1.1.x of this gem. 34 | For Rails 3 you need to use the version 1.2.x or master of this gem. 35 | For Rails 4 and 5 you need to use the version 1.3.x to 1.4.x. 36 | For Rails 6 you need to use version 1.5.x or master of this gem. 37 | 38 | ClassTableInheritance 39 | --------------------- 40 | 41 | This is an ActiveRecord plugin designed to allow 42 | simple multiple table (class) inheritance. 43 | 44 | This plugin was inspired by: 45 | inherits_from plugin => http://github.com/rwl4/inherits_from and 46 | Multiple Table Inheritance with ActiveRecord => http://mediumexposure.com/multiple-table-inheritance-active-record/ 47 | 48 | How to install 49 | -------------- 50 | 51 | gem install class-table-inheritance 52 | 53 | Example 54 | ------- 55 | 56 | ### Migrations 57 | 58 | ```ruby 59 | create_table :products do |t| 60 | t.string :description, :null => false 61 | t.string :subtype # Only if you need access of both side see example 62 | t.decimal :price 63 | t.timestamps 64 | end 65 | 66 | create_table :books, :inherits => :product do |t| 67 | t.string :author, :null => false 68 | end 69 | 70 | create_table :videos, :inherits => :product do |t| 71 | t.string :year, :null => false 72 | t.string :genre, :null => false 73 | end 74 | ``` 75 | 76 | ### Models 77 | 78 | ```ruby 79 | class Product < ActiveRecord::Base 80 | acts_as_superclass # only if you want top-down access. 81 | end 82 | 83 | class Book < ActiveRecord::Base 84 | inherits_from :product 85 | end 86 | 87 | class Video < ActiveRecord::Base 88 | inherits_from :product 89 | end 90 | 91 | book = Book.find(1) 92 | book.name => "Agile Development with Rails" 93 | book.author => "Dave Thomas" 94 | book.price => 19.00 95 | 96 | video = Video.find(2) 97 | video.name => "Inseption" 98 | video.year => "2010" 99 | video.genre => "SCI-FI" 100 | video.price => 22.00 101 | 102 | book = Book.new 103 | book.name = "Hamlet" 104 | book.author = "Shakespeare, William" 105 | book.price => 14.00 106 | book.save 107 | ``` 108 | 109 | Module inheritance 110 | ------------------ 111 | 112 | ### Migrations 113 | ```ruby 114 | create_table :mod_users do |t| 115 | t.string :name, :null => false 116 | end 117 | 118 | create_table :managers, :inherits => 'Mod::User' do |t| 119 | t.string :salary, :null => false 120 | end 121 | ``` 122 | 123 | ### Models 124 | 125 | ```ruby 126 | class Mod::User < ActiveRecord::Base 127 | end 128 | 129 | class Manager < ActiveRecord::Base 130 | inherits_from 'Mod::User' 131 | end 132 | ``` 133 | 134 | Top-down access (Polymorphic) 135 | ----------------------------- 136 | 137 | if you want to access product and get field in the subclass do you need to create a field subtype:string in superclass and ad acts_as_superclass in superclass and now you can do like this. 138 | 139 | ```ruby 140 | product = Product.find 1 # This is a Book instance. 141 | product.author 142 | 143 | product = Product.find 2 # This is a Video instance. 144 | product.genre 145 | ``` 146 | 147 | if you need help contanct me: bfscordeiro (at) gmail.com . 148 | 149 | 150 | Copyright (c) 2010 Bruno Cordeiro, released under the MIT license 151 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | require 'rake/testtask' 3 | Bundler::GemHelper.install_tasks 4 | 5 | # just 'rake test' 6 | Rake::TestTask.new do |t| 7 | t.libs << 'lib' 8 | t.libs << 'test' 9 | t.pattern = 'test/**/*_test.rb' 10 | t.verbose = true 11 | end 12 | 13 | task :default => :test 14 | -------------------------------------------------------------------------------- /class-table-inheritance.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require 'class-table-inheritance/version' 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "class-table-inheritance" 7 | s.version = ClassTableInheritance::VERSION 8 | s.platform = Gem::Platform::RUBY 9 | s.authors = ["Bruno Frank"] 10 | s.email = ["bfscordeiro@gmail.com"] 11 | s.homepage = "https://github.com/brunofrank/class-table-inheritance" 12 | s.summary = %q{ActiveRecord plugin designed to allow simple multiple table (class) inheritance.} 13 | s.description = %q{ActiveRecord plugin designed to allow simple multiple table (class) inheritance.} 14 | 15 | s.files = `git ls-files`.split("\n") 16 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 17 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 18 | s.require_paths = ["lib"] 19 | 20 | s.required_ruby_version = '>= 2.5', '< 4' 21 | 22 | s.add_runtime_dependency 'activerecord', '~>6.0' 23 | 24 | s.add_development_dependency 'minitest-reporters','~>1.1' 25 | s.add_development_dependency 'rake', '>=11' 26 | s.add_development_dependency 'sqlite3', '~>1.3' 27 | end 28 | -------------------------------------------------------------------------------- /init.rb: -------------------------------------------------------------------------------- 1 | require 'class-table-inheritance' -------------------------------------------------------------------------------- /lib/class-table-inheritance.rb: -------------------------------------------------------------------------------- 1 | require 'active_record' 2 | require 'class-table-inheritance/inherits-migration' 3 | require 'class-table-inheritance/class-table-inheritance' -------------------------------------------------------------------------------- /lib/class-table-inheritance/class-table-inheritance.rb: -------------------------------------------------------------------------------- 1 | 2 | # ClassTableInheritance is an ActiveRecord plugin designed to allow 3 | # simple multiple table (class) inheritance. 4 | class ActiveRecord::Base 5 | attr_reader :reflection 6 | 7 | def self.acts_as_superclass 8 | if self.column_names.include?("subtype") 9 | def self.find(*args) 10 | super_classes = super 11 | begin 12 | if super_classes.kind_of? Array 13 | super_classes.map do |item| 14 | if !item.subtype.nil? && !item.subtype.blank? 15 | inherits_type = super_classes.subtype.to_s.classify.constantize 16 | inherits_type.send(:find, item.id) 17 | else 18 | super_classes 19 | end 20 | end 21 | else 22 | if !super_classes.subtype.nil? && !super_classes.subtype.blank? 23 | inherits_type = super_classes.subtype.to_s.classify.constantize 24 | inherits_type.send(:find, *args) 25 | else 26 | super_classes 27 | end 28 | end 29 | rescue 30 | super_classes 31 | end 32 | end 33 | end 34 | end 35 | 36 | def self.inherits_from(association_id) 37 | # Subst the module simbol to dash and if this is string 38 | if association_id.kind_of?(String) 39 | class_name = association_id 40 | association_id = association_id.to_s.gsub(/::/, '_').downcase.to_sym 41 | else 42 | class_name = association_id.to_s.classify 43 | end 44 | 45 | # add an association 46 | belongs_to association_id, :class_name => class_name, :dependent => :destroy 47 | 48 | 49 | # set the primary key, it' need because the generalized table doesn't have 50 | # a field ID. 51 | self.primary_key = "#{association_id}_id" 52 | 53 | 54 | # Autobuild method to make an instance of association 55 | m = const_set("#{association_id.to_s.camelize}Builder", Module.new) 56 | m.send(:define_method, association_id) do 57 | super() || send("build_#{association_id}") 58 | end 59 | prepend(m) 60 | 61 | # bind the before save, this method call the save of association, and 62 | # get our generated ID an set to association_id field. 63 | before_save :save_inherit 64 | 65 | 66 | # Bind the validation of association. 67 | validate :inherit_association_must_be_valid 68 | 69 | # Generate a method to validate the field of association. 70 | define_method("inherit_association_must_be_valid") do 71 | association = send(association_id) 72 | 73 | unless valid = association.valid? 74 | association.errors.each do |attr, message| 75 | errors.add(attr, message) 76 | end 77 | end 78 | 79 | valid 80 | end 81 | 82 | 83 | 84 | # get the class of association by reflection, this is needed because 85 | # i need to get the methods and attributes to make a proxy methods. 86 | association_class = class_name.constantize 87 | # Get the colluns of association class. 88 | inherited_columns = association_class.column_names 89 | # Make a filter in association colluns to exclude the colluns that 90 | # the generalized class already have. 91 | inherited_columns = inherited_columns.reject { |c| self.column_names.grep(c).length > 0 || c == "type" || c == "subtype"} 92 | # Get the methods of the association class and tun it to an Array of Strings. 93 | inherited_methods = association_class.reflections.map { |key,value| key.to_s } 94 | # Make a filter in association methods to exclude the methods that 95 | # the generalizae class already have. 96 | inherited_methods = inherited_methods.reject { |c| self.reflections.map {|key, value| key.to_s }.include?(c) } 97 | 98 | 99 | # create the proxy methods to get and set the properties and methods 100 | # in association class. 101 | (inherited_columns + inherited_methods).each do |name| 102 | define_method name do 103 | # if the field is ID than i only bind that with the association field. 104 | # this is needed to bypass the overflow problem when the ActiveRecord 105 | # try to get the id to find the association. 106 | if name == 'id' 107 | self["#{association_id}_id"] 108 | else 109 | assoc = send(association_id) 110 | assoc.send(name) 111 | end 112 | end 113 | 114 | 115 | define_method "#{name}=" do |new_value| 116 | # if the field is ID than i only bind that with the association field. 117 | # this is needed to bypass the overflow problem when the ActiveRecord 118 | # try to get the id to find the association. 119 | if name == 'id' 120 | self["#{association_id}_id"] = new_value 121 | else 122 | assoc = send(association_id) 123 | assoc.send("#{name}=", new_value) 124 | end 125 | end 126 | end 127 | 128 | 129 | # Create a method do bind in before_save callback, this method 130 | # only call the save of association class and set the id in the 131 | # generalized class. 132 | define_method("save_inherit") do |*args| 133 | association = send(association_id) 134 | if association.attribute_names.include?("subtype") 135 | association.subtype = self.class.to_s 136 | end 137 | association.save 138 | self["#{association_id}_id"] = association.id 139 | true 140 | end 141 | end 142 | end 143 | -------------------------------------------------------------------------------- /lib/class-table-inheritance/inherits-migration.rb: -------------------------------------------------------------------------------- 1 | module InheritsMigration 2 | # Generate the association field. 3 | def create_table(table_name, options = {}, &block) 4 | options[:id] ||= false if options[:inherits] 5 | 6 | super(table_name, options) do |table_defintion| 7 | if options[:inherits] 8 | if options[:inherits].kind_of?(String) 9 | column_to_create = options[:inherits].gsub(/::/, '_').downcase 10 | association_type = options[:inherits].constantize 11 | else 12 | column_to_create = options[:inherits] 13 | association_type = options[:inherits].to_s.classify.constantize 14 | end 15 | association_inst = association_type.send(:new) 16 | attr_column = association_inst.column_for_attribute(association_type.primary_key) 17 | 18 | field_option = {:primary_key => true, :null => false} 19 | field_option[:limit] = attr_column.limit if attr_column.limit 20 | table_defintion.column "#{column_to_create}_id", attr_column.type, field_option 21 | end 22 | yield table_defintion 23 | end 24 | end 25 | end 26 | 27 | ActiveRecord::ConnectionAdapters::SchemaStatements.prepend(InheritsMigration) 28 | -------------------------------------------------------------------------------- /lib/class-table-inheritance/version.rb: -------------------------------------------------------------------------------- 1 | class ClassTableInheritance 2 | VERSION = "1.5.0" 3 | end 4 | -------------------------------------------------------------------------------- /test/class_table_inheritance_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ClassTableInheritanceTest < Minitest::Test 4 | 5 | def test_create 6 | name = 'Bike' 7 | 8 | product = Product.new 9 | product.name = name 10 | assert_equal true, product.save 11 | 12 | product = Product.find product.id 13 | assert_equal name, product.name 14 | end 15 | 16 | 17 | def test_inheritance_book 18 | title = 'Atlas Shrugged' 19 | isbn = '9780451191144' 20 | 21 | book = Book.new 22 | book.name = title 23 | book.isbn = isbn 24 | assert_equal true, book.save 25 | 26 | book = Book.find book.id 27 | assert_equal title, book.name 28 | assert_equal isbn, book.isbn 29 | end 30 | 31 | def test_inheritance_video 32 | name = 'Amy Whinehouse - Rehab' 33 | url = 'http://www.youtube.com/watch?v=3QI8RjKibyc' 34 | 35 | video = Mod::Video.new 36 | video.name = name 37 | video.url = url 38 | assert_equal true, video.save 39 | 40 | video = Mod::Video.find video.id 41 | assert_equal name, video.name 42 | assert_equal url, video.url 43 | end 44 | 45 | def test_inheritance_user_save 46 | name = 'bfscordeiro' 47 | 48 | user = Mod::User.new 49 | user.name = name 50 | assert_equal true, user.save 51 | 52 | user = Mod::User.find user.id 53 | assert_equal name, user.name 54 | end 55 | 56 | def test_inheritance_manager_save 57 | name = 'bfscordeiro' 58 | salary = '6000' 59 | 60 | manager = Manager.new 61 | manager.name = name 62 | manager.salary = salary 63 | assert_equal true, manager.save 64 | 65 | manager = Manager.find manager.id 66 | assert_equal name, manager.name 67 | assert_equal salary, manager.salary 68 | end 69 | 70 | 71 | end 72 | -------------------------------------------------------------------------------- /test/database.yml: -------------------------------------------------------------------------------- 1 | sqlite3: 2 | :adapter: sqlite3 3 | :database: test/cti_plugin.sqlite3 4 | -------------------------------------------------------------------------------- /test/models/book.rb: -------------------------------------------------------------------------------- 1 | class Book < ActiveRecord::Base 2 | inherits_from :product 3 | end -------------------------------------------------------------------------------- /test/models/manager.rb: -------------------------------------------------------------------------------- 1 | class Manager < ActiveRecord::Base 2 | inherits_from 'Mod::User' 3 | end -------------------------------------------------------------------------------- /test/models/mod.rb: -------------------------------------------------------------------------------- 1 | module Mod 2 | def self.table_name_prefix 3 | 'mod_' 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/models/mod/user.rb: -------------------------------------------------------------------------------- 1 | class Mod::User < ActiveRecord::Base 2 | end -------------------------------------------------------------------------------- /test/models/mod/video.rb: -------------------------------------------------------------------------------- 1 | class Mod::Video < ActiveRecord::Base 2 | inherits_from :product 3 | end -------------------------------------------------------------------------------- /test/models/product.rb: -------------------------------------------------------------------------------- 1 | class Product < ActiveRecord::Base 2 | end -------------------------------------------------------------------------------- /test/schema.rb: -------------------------------------------------------------------------------- 1 | ActiveRecord::Schema.define(:version => 0) do 2 | create_table :products, :force => true do |t| 3 | t.string :name 4 | end 5 | 6 | create_table :books, :force => true do |t| 7 | t.string :isbn 8 | t.integer :product_id 9 | end 10 | 11 | create_table :mod_videos, :force => true do |t| 12 | t.integer :product_id 13 | t.string :url 14 | end 15 | 16 | create_table :mod_users, :force => true do |t| 17 | t.string :name 18 | end 19 | 20 | create_table :managers, :force => true do |t| 21 | t.integer :mod_user_id 22 | t.string :salary 23 | end 24 | end -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'minitest/autorun' 3 | require 'minitest/reporters' 4 | require 'active_record' 5 | require 'class-table-inheritance' 6 | require 'yaml' 7 | 8 | Minitest::Reporters.use! 9 | 10 | database = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml')) 11 | ActiveRecord::Base.establish_connection(database['sqlite3']) 12 | load(File.dirname(__FILE__) + "/schema.rb") if !File.exist?(database['sqlite3'][:database]) 13 | 14 | require 'models/product' 15 | require 'models/book' 16 | require 'models/mod' 17 | require 'models/mod/video' 18 | require 'models/mod/user' 19 | require 'models/manager' --------------------------------------------------------------------------------