Search code examples
rubysinatramodel-associationsruby-datamapper

Ruby DataMapper: model associations review, model updates after finalize, and associating slightly autonomous records


I am brand new to Ruby and even newer to DataMapper, or any ORM for that matter. I come from a Perl background and am not really an OOP type of developer. So I'm wandering in unknown territory here. Apologies for the long-winded question...

In this project, I have the concept of deviceclasses and devices which will be mapped under the deviceclasses. Deviceclasses need to be able to have child deviceclasses. The common root deviceclass names (in other words the root from which all deviceclasses come) are called "FOO" or "BAR" (in this example code) and each of those can have an arbitrary set of children devicesclasses. Finally, deviceclasses eventually contain devices.

So: Deviceclasses have many deviceclasses Deviceclasses have many devices A deviceclass has one deviceclass_name many devices belong to a deviceclass

So, IE:

FOO
    JOHNSHOUSE
         UPSTAIRS
             device1
             device2
         DOWNSTAIRS
             device1
             device2
    MANYHOUSES
        JOE
            GARAGE
                device1
                device2
        SUZY
            BEDROOM
                device1
                device2
                device3
        TIM
            LIVINGROOM
                device1
    ARBITRARY
        device1
        SOMEPLACE
            device1
            device2
BAR
    ENGLAND
        LONDON
            MYHOUSE
                BEDROOM
                    device1
                    device2
                    device3

And here's where I'm getting stuck...devices and deviceclasses must be able to be autonomously added to the DB and their associations will be performed later. So, I can't do

deviceclass = MyDB::Deviceclass.new
device      = MyDB::Device
deviceclass.device.new(blah)

My module, which contains the pertinent models for which I'm basing this set of questions...

Question 1 - Am I doing this right? Note that self.validate_root_deviceclasses method under Deviceclass. I have to have the n root deviceclasses in the DB before anything else so this method creates them. Unfortunately, the property update does not work. I would love some direction on that.

module DeviceDB
    ROOT_DEVICECLASSES %w{FOO BAR}

    class Deviceclass
        include DataMapper::Resource
        property :id, Serial
        property :hw_id, String,                :unique  => true
        property :root_deviceclass, Boolean,    :default => false
        property :parent_deviceclass_id, Integer
        property :deviceclass_name, String
        property :updated_at, DateTime
        property :created_at, DateTime

        has n, :devices,       :through => Resource
        has n, :deviceclasses, :through => Resource
        has 1, :deviceclass, self, {:through=>:deviceclasses, :via=>:parent_deviceclass_id}

        def self.validate_root_deviceclasses
            root_deviceclasses = all(:root_deviceclass => true)

            if root_deviceclasses.count > 0 
# get whats in the db now
                db      = Array.new(root_deviceclasses.map(&:deviceclass_name))
# match it against the global list (top of this file)
                missing = ROOT_DEVICECLASSES.map{|root| root unless db.grep(/#{root}/i)[0]}.compact

# if something's missing, add it.
                missing.each do |missing|
                    begin
                        create(:deviceclass_name => missing, :root_deviceclass => true).save
                    rescue DataMapper::SaveFailureError => e
                        @error = [e.resource.errors.map{|err| err}].join(', ')
                        return(false)
                    end 
                end 
            else
                begin
                    ROOT_DEVICECLASSES.each do |root|
                        create(:deviceclass_name => root, :root_deviceclass => true).save
                    end 
                rescue DataMapper::SaveFailureError => e
                    @error = [e.resource.errors.map{|err| err}].join(', ')
                    return(false)
                end 
            end 

            begin
                default = first(:deviceclass_name => 'PTS').id

                property :parent_deviceclass_id, Integer, :default => default # fail
                DataMapper.finalize                                           # fail
                return(self)
            rescue DataMapper::SaveFailureError => e
                @error = [e.resource.errors.map{|err| err}].join(', ')
            end

            return(true)
        end
    end

    class Device
        include DataMapper::Resource
        property :id, Serial
        property :deviceclass_id, Integer
        property :device_id, String, :unique => true
        property :devicename, String
        ... more properties...
        property :updated_at, DateTime
        property :created_at, DateTime

        belongs_to :deviceclass, :required => false
    end

    DataMapper.finalize
    DataMapper.auto_upgrade!

    Deviceclass.validate_root_deviceclasses
end

Question2: is there some magical way to associate the deviceclasses and devices or do I need to do it the tough way by grabbing an id of the device and associating it via update to an associated deviceclass?

Question3: Is there a way I can add a property to the model after the table has already been migrated which would effectively change the table by adding :default (see the fail case above). If not, is there any way I can obtain my default value during the creation of the model. Lambda comes to mind but that would only work if the table already exists and the ROOT_DEVICENAMES have already been added.


Solution

  • For the first question about the schema and adding items autonomously you might consider using dm-is-tree for your Deviceclass it'll do a lot of the work for you. Below is an example of creating an item and 'later on' adding associated items.

    require 'rubygems'
    require 'data_mapper'
    require 'dm-is-tree'
    
    # setup
    DataMapper::Logger.new($stdout, :debug)
    DataMapper.setup(:default, 'sqlite::memory:')
    
    # models
    class Deviceclass
      include DataMapper::Resource
    
      property :id,          Serial
      property :hw_id,       String,   :unique  => true
      property :name,        String
      property :updated_at,  DateTime
      property :created_at,  DateTime
    
      is :tree, :order => :name
      has n, :devices
    end
    
    class Device
      include DataMapper::Resource
    
      property :id,               Serial
      property :device_class_id,  Integer
      property :name,             String
      property :updated_at,       DateTime
      property :created_at,       DateTime
    
      belongs_to :deviceclass, :required => false
    end
    
    # go!
    DataMapper.finalize
    DataMapper.auto_upgrade!
    
    # make the root deviceclass
    parent = Deviceclass.create(:name => "Root")
    
    # later on make a child
    child = Deviceclass.create(:name => "Child")
    # and add it to the parent
    parent.children << child
    
    # again later, create some devices
    d1 = Device.create(:name => "D1")
    d2 = Device.create(:name => "D2")
    
    # add them
    parent.devices << d1
    child.devices << d2
    
    # get stuffs
    puts parent.children
    puts child.root
    puts parent.devices
    puts child.devices
    

    I'm not sure it's a good idea to use validation to generate missing Deviceclasses. If the initial data is not constantly changing I'd run a seed script on startup. You can use something like dm-sweatshop to seed the db.

    I think I need a few more details on #3 do you want a default name for a Deviceclass (you can add :default => 'foo') which you might know as you are using :default already! :)