8

I have an Item class with the following attributes:

itemId,name,weight,volume,price,required_skills,required_items.

Since the last two attributes are going to be multivalued, I removed them and create new schemes like:

itemID,required_skill (itemID is foreign key, itemID and required_skill is primary key.)

Now, I'm confused how to create/use this new table. Here are the options that came to my mind:

1) The relationship between Items and Required_skills is one-to-many, so I may create a RequiredSkill class, which belongs_to Item, which in turn has n RequiredSkills. Then I can do Item.get(1).requiredskills. This sounds most logical to me and provides me methods and queries like:

sugar.components.create :raw_material => water.name
sugar.components.create :raw_material => sugarcane.name
sugar.components
sugar.components.count

2) Since required_skills may well be thought of as constants (since they resemble rules), I may put them into a hash or gdbm database or another sql table and query from there, which I don't prefer.

My question is: is there something like a modelless table in datamapper, where datamapper is responsible from the creation and integrity of the table and allows me to query it in datamapper way, but does not require a class, like I may do it in sql?

I solved my problem by using the first way: I created a new class for each normalization process (which appears as one-to-many association above). However, I am new to object oriented programming and I don't know if creating a new class for each such normalization is the usual way to do it in datamapper, or rather a hack? This, and whether there is a better way to do this, is what I would like to know.

@JustinC

Rereading the datamapper.org Associations several times, now I see that datamapper certainly requires separate classes for joins. So you answered my question. However, since Robert Harvey placed the bounty, I feel the responsibility to wait a little more for a response about a dynamic way.

Your code complained with Cannot find the child_model Container for Item in containers. I managed to get it work with the second example of self referential association like below (putting here as a reference to others):

class Item
  class Link
    include DataMapper::Resource
    storage_names[:default] = "requirement_links"

    belongs_to :require, "Item", :key => true
    belongs_to :required, "Item", :key => true
  end

  include DataMapper::Resource

  property :id, Serial
  property :name, String, :required => true

  has n, :links_to_required_items, "Item::Link", :child_key => [:require_id]
  has n, :links_to_requiring_items, "Item::Link", :child_key => [:required_id]

  has n, :required_items, self,
    :through => :links_to_required_items,
    :via => :required
  has n, :requiring_items, self,
    :through => :links_to_requiring_items,
    :via => :require

 def require(others)
    required_items.concat(Array(others))
    save
    self
  end

  def unrequire(others)
    links_to_required_items.all(:required => Array(others)).destroy!
    reload
    self
  end
end

So I can do:

jelly = Item.get :name => "Jelly"
sugar = Item.get :name => "Sugar"
jelly.require sugar

to require items and:

jelly.required_items.each { |i|; puts i.name }

to list requirements, which are really great.

After reading your answer, I see that I am yet to normalize my database schema further. To be honest, I don't see the point of defining the relationship between rawmaterials and products as self referential. I mean, if that would be a small program, I certainly would use a hash like {:jelly => ":sugar => 3, :water => 5"} to reflect required items and amounts, according to YAGNI principle. Doing it as in the 1st option already provides me queries and methods as simple as the ones provided by the self referential association. (However, I must admit that it looks more like a stored procedure rather than a method call to an object.)

So, could you very mind to explain the benefits of using a self referential association, which is difficult to understand/implement for me, compared to my simpler approach? I am new to OOP and wonder if I'm sort of undermodelling.

1 Answer 1

2
+200

I think what you are looking for at a SQL conceptual level is a junction table (map, link, resolver, pivot are also common names for handling many to many relationships). These junction tables are generally intermediary tables; however, additional attributes can and are often added to them.

The intention of the stated pseudo schema is a bit murky, but I think what you intended is that items can require multiple skills; items can also require other items, each with their own required skills, possibly own requisite items, and so and so forth, to many levels deep. Beware of circular references in your [many-to-many] self-referencing relationships, such as what could happen in 'containerItemMaps'. The following pseudo schema reflects how I envision the OP intent:

items (itemId PK, itemName, weight, volume, price)

skillMaps ( (itemId, skillId) PK)

skills (skillId PK, skillName)

containerItemMaps ( (containerItemId, componentItemId) PK)
    -- containerItemId is the parent/requiring item id
    -- componentItemId is the child/required item id

ActiveRecord terminology suggests 'has_and_belongs_to_many' as the type of association a relationship in a data model to use in this situation. For more information you may want to look at the page at datamapper.org Associations. Specifically the sections titled 'Has, and belongs to many (Or Many-To-Many)' and 'Self referential many to many relationships'

Because I am not a ruby guy at this point, I can only muddle with ruby code to give an example, but this is my best approximation of what your item class would look like:

# revised
class Item
   include DataMapper::Resource

   property :id, Serial

   property :name, String, :required => true
   property :weight, Float
   property :volume, Float
   property :price, Float 

   has n, :componentMaps, :child_key => [:container_id]
   has n, :components, self, :through => :componentMaps, :via => :component

   has n, :skillMaps, :child_key => [:skill_id]
   has n, :skills, :through => :skillMaps, :via => :skill    
end

And the map table for self-referencing many to many items, eg.. required items:

#revised
class ComponentMap
  include DataMapper::Resource

  belongs_to :container, 'Item', :key => true
  belongs_to :component, 'Item', :key => true

  property :quantity, Integer, :default => 1
end

For completeness:

class SkillMap
  include DataMapper::Resource

  belongs_to :item, 'Item', :key => true
  belongs_to :skill, 'Skill', :key => true

  property :mastery, Enum[:none, :aprentice, :journeyman, :craftsman, :master ], :default => :none

end

class Skill
    include DataMapper::Resource

    property :id, Serial
    property :name, String, :required => true

    has n, :skillMap, :child_key =>[:skill_id]     
end

Revisions:

Noting your concerns, I went and installed an interpruter and debugger to verify the code compiled and the emitted sql was more ideal. Originally, I only was going off cursory examination of documentation. The structures above should produce generally well-structured sql from the mappings.

No matter which fields and structures you use, and no matter which ORM you pick (datamapper or some other provider), you will want to run the code through the debugger and pay attention to the sql it emits as sometimes the mappings aren't necessarily what you might first expect.

A second note about the junction tables (skillMap and componentMap): note my inclusion of additional fields (quantity and mastery). These seem to be a natural fit for the kind of application originally described, even though not originally specified. In a recipe, some ingredients are common among many different combinations, however the quantity from recipe to recipe varies. For skills, like ingredients, the skill level needed to perform certain activities varies, and thus I added a mastery field to the junction table skillMap.

Of course, you probably want to add appropriate business rules and helper functions (for accessing the composition of collections programmattically such as adding and removing elements, adding and removing groups of elements, and so on).

Hopefully this demonstrates a bit better a reason why you may want to consider and use the junction table over a straight hash. Of course each specific application is different, and perhaps the ability to specify additional aspects of the relationship between items and skill and items and other items isnt needed in your case.

Having and utilizing the extra control in defining the relationships explicitly has many advantages over relying on a dynamic/magic mapping. In some cases, I feel it is really needed, and I think in the case of a many-to-many, this is demonstrated. For one-to-many, the relationship is easier to infer, and using a more dynamic method of generating the mappings (eg. has n, : <attribute group>, :through => Resource) would be acceptable.

3
  • Please see my edit above. Sorry that I couldn't express it in shorter words.
    – barerd
    Commented Oct 27, 2012 at 16:01
  • Thanks a lot. Over the 2 days I compared several options for an inventory. I saw that by using has n, :items, :through => Inventory approach, I got more efficient queries compared to a hash like approach. What debugger did you use by the way?
    – barerd
    Commented Oct 30, 2012 at 15:13
  • For the sql portions: 'DataMapper::Logger.new($stdout, :debug)' And on the ruby side, the gem 'ruby-debug'; both from within eclipse
    – JustinC
    Commented Oct 30, 2012 at 15:31

Not the answer you're looking for? Browse other questions tagged or ask your own question.