Search code examples
ruby-on-railssingle-table-inheritance

STI and has_many association with "type" column as Key


I am using Single Table Inheritance for managing different types of projects. I decided to store some information associated with each project type. So i created new table "project_types" with "model_type" field as primary key. Primary key values are values of "type" field of "projects" table. Problem: When i trying to get associated with Project object ProjectTypes object it always returns null.

>> p = Project.find(:first)
=> #<SiteDesign id: 1, type: "SiteDesign", name: "1", description: "dddd", concept: "d", client_id: 40, created_at: "2009-10-15 08:17:45", updated_at: "2009-10-15 08:17:45">
>> p.project_type
=> nil

Getting projects associated with ProjectTypes project is OK. Is there way to make it works properly?

Models:

class Project < ActiveRecord::Base
    belongs_to :project_type, :class_name => "ProjectTypes", :foreign_key => "model_name"
end

class SiteDesign < Project
end

class TechDesign < Project
end

class ProjectTypes < ActiveRecord::Base
  self.primary_key = "model_name"
  has_many :projects, :class_name => "Project", :foreign_key => "type"
end

Migrations:

class CreateProjectTypes < ActiveRecord::Migration
  def self.up
    create_table :project_types, :id => false  do |t|
      t.string :model_name , :null => false
      t.string :name, :null => false
      t.text :description

      t.timestamps
    end

    add_index :project_types, :model_name, :unique => true


    #all project types that are used.
    models_names = {"SiteDesign" => "Site design",
      "TechDesign" => "Tech design"}

    #key for model_name and value for name
    models_names.each do |key,value|
      p = ProjectTypes.new();
      p.model_name = key
      p.name = value
      p.save
    end

  end

  def self.down
    drop_table :project_types
  end
end

class CreateProjects < ActiveRecord::Migration
  def self.up
    create_table :projects do |t|
      t.string :type
      t.string :name
      t.text :description
      t.text :concept
      t.integer :client_id

      t.timestamps
    end
  end

  def self.down
    drop_table :projects
  end
end

Solution

  • Not surprising you're getting problems. By moving from a pure STI system to your current system you are horribly breaking the patterns you are using by intermingling parts of one with parts of another.

    I'd personally go for something like:

    class Project < ActiveRecord::Base
        attr_readonly(:project_type)
        belongs_to :project_type
        before_create :set_project_type
    
        def set_project_type()
            project_type = ProjectType.find_by_model_name(this.class)
        end
    end
    
    class SiteProject < Project
    end
    
    class TechProject < Project
    end
    
    class ProjectType < ActiveRecord::Base
        has_many :projects
    end
    

    with migrations:

    class CreateProjectTypes < ActiveRecord::Migration
      def self.up
        create_table :project_types  do |t|
          t.string :model_name , :null => false
          t.string :name, :null => false
          t.text :description
    
          t.timestamps
        end
    
        add_index :project_types, :model_name, :unique => true
    
    
        #all project types that are used.
        models_names = {"SiteDesign" => "Site design",
          "TechDesign" => "Tech design"}
    
        #key for model_name and value for name
        models_names.each do |key,value|
          p = ProjectTypes.new();
          p.model_name = key
          p.name = value
          p.save
        end
    
      end
    
      def self.down
        drop_table :project_types
      end
    end
    
    class CreateProjects < ActiveRecord::Migration
      def self.up
        create_table :projects do |t|
          t.string :type
          t.references :project_type, :null => false
          t.text :description
          t.text :concept
          t.integer :client_id
    
          t.timestamps
        end
      end
    
      def self.down
        drop_table :projects
      end
    end
    

    It just cleans things up and it also helps clarify what you're doing. Your 'ProjectType' table is purely for extra data, your inheritance tree still exists. I've also thrown in some checks to make sure your project type is always set (and correctly, based on the model name) and stops you from changing project type once it's been saved by making the attribute read only.