Agazoom
Agazoom

Reputation: 618

How do I build a complex nested form to represent dynamic attributes

I've got a data model represented by the following image: enter image description here

Basically, the idea is that there are many different Collections. All the Items in a single collection will have the same Attributes, but the list of attributes will be different per collection. The value of each Attribute per item will be stored in Item Attribute Values.

I'm trying to build a single page where a user can populate the attributes for an item. I'm assuming a nested form is the way to go but I'm at a loss as to how to represent this in the controller and on the page, considering the names of the attributes are in one table and the values in another.

If anyone has encountered or had to deal with a similar situation, any help would be appreciated.

Thanks

Upvotes: 1

Views: 50

Answers (1)

max
max

Reputation: 102368

Here is one potential solution.

class Collection
  has_and_belongs_to_many :items
  has_and_belongs_to_many :attributes
end

class Item
  has_and_belongs_to_many :collections
  has_many :item_attributes
  has_many :attributes, though: :item_attributes
end

class Attributes
  has_and_belongs_to_many :collections
  has_many :item_attributes
  has_many :items, though: :item_attributes
end

class ItemAttribute
  belongs_to :item
  belongs_to :attribute
end

So lets look at the database layout to back these models:

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

  create_table "attributes_collections", id: false, force: :cascade do |t|
    t.integer "attribute_id",  null: false
    t.integer "collection_id", null: false
  end

  create_table "collections", force: :cascade do |t|
    t.string   "title"
    t.integer  "user_id"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

  add_index "collections", ["user_id"], name: "index_collections_on_user_id"

  create_table "collections_items", id: false, force: :cascade do |t|
    t.integer "collection_id", null: false
    t.integer "item_id",       null: false
  end

  create_table "item_attributes", force: :cascade do |t|
    t.integer  "item_id"
    t.integer  "attribute_id"
    t.string   "value"
    t.datetime "created_at",   null: false
    t.datetime "updated_at",   null: false
  end

  add_index "item_attributes", ["attribute_id"], name: "index_item_attributes_on_attribute_id"
  add_index "item_attributes", ["item_id"], name: "index_item_attributes_on_item_id"

  create_table "items", force: :cascade do |t|
    t.string   "name"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

end

A Better way?

But of course performance will suffer due to the many joins. Plus each attribute will be stored as a VARCHAR which means you can't do any numeric comparisons in the database.

If you really need a flexible schema i would instead look into using HSTORE, JSON or another dynamic column type or a schemaless database such as MongoDB.

How do I create a form / controller for this?

First you should get very acquainted with what can be done with accepts_nested_attributes_for and fields_for and maybe consider using AJAX to delegate the actions out to CollectionController and a ItemController on the back end rather than cramming it all into a single monstrosity.

Upvotes: 1

Related Questions