Rails отношения ActiveRecord, STI и наследование

Итак, прошло много лет с тех пор, как я написал любой код ruby, и мой дизайн может быть неправильным. Имея это в виду, я пишу небольшую утилиту для клонирования сущностей проекта в TargetProcess через REST. Целевой процесс имеет модель данных, которая допускает несколько типов родительских: дочерних отношений:

project:epic:feature:user_story
project:feature:user_story
project:user_story

Однако все сущности практически идентичны с точки зрения структуры данных, поэтому представляется целесообразным использовать STI и модели для определения отношений и наследования. Я создал новое приложение Rails только с этими моделями, чтобы проверить ошибку, которую я получаю при попытке связать Epic с Feature:

ActiveModel::MissingAttributeError: can't write unknown attribute `epic_id`

Вот модели:

class TargetProcessEntity < ActiveRecord::Base
end

class Project < TargetProcessEntity
  has_many :epics
  has_many :features
  has_many :user_stories
end

class Project < TargetProcessEntity
  has_many :epics
  has_many :features
end

class Epic < TargetProcessEntity
  belongs_to :project
  has_many   :features
end

class Feature < TargetProcessEntity
  belongs_to :project
  belongs_to :epic
  has_many   :user_stories
end

class UserStory < TargetProcessEntity
  belongs_to :feature
  belongs_to :project
end

Вот схема:

ActiveRecord::Schema.define(version: 20150929122254) do

  create_table "epics", force: :cascade do |t|
    t.datetime "created_at",               null: false
    t.datetime "updated_at",               null: false
    t.integer  "target_process_entity_id"
    t.integer  "project_id"
  end

  add_index "epics", ["project_id"], name: "index_epics_on_project_id"
  add_index "epics", ["target_process_entity_id"], name: "index_epics_on_target_process_entity_id"

  create_table "features", force: :cascade do |t|
    t.integer "project_id"
    t.integer "epic_id"
    t.integer "target_process_entity_id"
  end

  add_index "features", ["epic_id"], name: "index_features_on_epic_id"
  add_index "features", ["project_id"], name: "index_features_on_project_id"
  add_index "features", ["target_process_entity_id"], name: "index_features_on_target_process_entity_id"

  create_table "projects", force: :cascade do |t|
    t.datetime "created_at",               null: false
    t.datetime "updated_at",               null: false
    t.integer  "target_process_entity_id"
  end

  add_index "projects", ["id"], name: "index_projects_on_id"
  add_index "projects", ["target_process_entity_id"], name: "index_projects_on_target_process_entity_id"

  create_table "target_process_entities", force: :cascade do |t|
    t.string   "type",             null: false
    t.string   "name"
    t.text     "description"
    t.integer  "source_remote_id"
    t.float    "numeric_priority"
    t.integer  "owner"
    t.datetime "created_at",       null: false
    t.datetime "updated_at",       null: false
    t.integer  "cloned_remote_id"
    t.string   "resource_type"
    t.integer  "project_id"
  end

  create_table "user_stories", force: :cascade do |t|
    t.integer  "project_id"
    t.integer  "feature_id"
    t.datetime "created_at",               null: false
    t.datetime "updated_at",               null: false
    t.integer  "target_process_entity_id"
  end

  add_index "user_stories", ["feature_id"], name: "index_user_stories_on_feature_id"
  add_index "user_stories", ["project_id"], name: "index_user_stories_on_project_id"
  add_index "user_stories", ["target_process_entity_id"], name: "index_user_stories_on_target_process_entity_id"

end

Хотя Epic и Feature оба имеют project_id, экземпляр Feature не имеет атрибута epic_id; попытка назначить эпопею на взрывы:

[20] pry(main)> epic = Epic.new
=> #<Epic:0x007fcab6c80590
 id: nil,
 type: "Epic",
 name: nil,
 description: nil,
 source_remote_id: nil,
 numeric_priority: nil,
 owner: nil,
 created_at: nil,
 updated_at: nil,
 cloned_remote_id: nil,
 resource_type: "Epic",
 project_id: nil>
[21] pry(main)> feature = Feature.new
=> #<Feature:0x007fcab6d3ba48
 id: nil,
 type: "Feature",
 name: nil,
 description: nil,
 source_remote_id: nil,
 numeric_priority: nil,
 owner: nil,
 created_at: nil,
 updated_at: nil,
 cloned_remote_id: nil,
 resource_type: "Feature",
 project_id: nil>
[22] pry(main)> epic.save
   (0.1ms)  begin transaction
  SQL (0.3ms)  INSERT INTO "target_process_entities" ("type", "resource_type", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["type", "Epic"], ["resource_type", "Epic"], ["created_at", "2015-10-02 15:18:13.351578"], ["updated_at", "2015-10-02 15:18:13.351578"]]
   (4.6ms)  commit transaction
=> true
[23] pry(main)> feature.epic = epic
ActiveModel::MissingAttributeError: can't write unknown attribute `epic_id`
from /Users/kcallahan/.rbenv/versions/2.0.0-p647/lib/ruby/gems/2.0.0/gems/activerecord-4.2.4/lib/active_record/attribute.rb:138:in `with_value_from_database'
[24] pry(main)> 

Я понимаю, что очень возможно, что я делаю что-то не так или принял плохое дизайнерское решение; любая информация очень ценится, так как я не смог ничего найти по этому поводу и несколько дней бился головой об это!

2 ответа

ОК, я получил это почти случайно! Я добавил столбцы xxx_id в таблицу target_process_entities. Я предполагал, что таблицы ИППП смогут отвечать на определения отношений, хотя мое понимание внутренней работы ИППП и отношений в лучшем случае ржавое и неполное...

Я могу ошибаться, но похоже, что ваша таблица Feature является таблицей соединений для многих и многих отношений между Project и Epic.

Если это так, ваши модели могут выглядеть так

class Project < TargetProcessEntity
  has_many :features
  has_many :epics, through: :features
end

class Epic < TargetProcessEntity
  has_many :features
  has_many :projects, through: :features
end

class Feature < TargetProcessEntity
  belongs_to :project
  belongs_to :epic
  has_many   :user_stories
end

источник подразумевается, если вы используете то же имя

http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html

Другие вопросы по тегам