Databases & Dynamic Ruby Classes

David uses Ruby's flexibility to execute Ruby code from a MySQL database.


December 01, 2004
URL:http://www.drdobbs.com/database/databases-dynamic-ruby-classes/184405925

Ruby, an open-source scripting language designed by Yukihiro Matsumoto, is an elegant language that follows the Principal of Least Surprise—things are designed to work the way that you expect them to work.

Despite this elegance—or perhaps because of it—Ruby is a flexible language. For example, class definitions are executable code; they are evaluated at run-time, not at compile time. Ruby uses this flexibility well. The language includes a shortcut for creating accessor methods that generates method definitions at runtime. The shortcut actually generates the code that defines these methods, and the Ruby interpreter executes it at runtime; it takes advantage of Ruby's ability to run any arbitrary string containing Ruby code.

Ruby also lets you modify its behaviors to an uncommon degree; for example, you can define a method that handles when a nonexistent method is called on an object. Ruby also has a flexible mechanism for running code—any string containing Ruby code—in an arbitrary context.

Ruby is a highly portable language that runs on Windows, Linux, UNIX, Mac OS, BeOS, and other platforms. For more information, see Programming in Ruby, by Dave Thomas and Andy Hunt (DDJ, January 2001) and http://www.ruby-lang.org/. In this article, I use Ruby's features to run Ruby code from a MySQL database (although most of the major databases could have been used). In doing so, I first examine how to trap calls to missing methods and determine whether an object has a given property using a single table and one row per object. Next, I look at how to expand this methodology so that any given object property can evaluate to either a static value or can return the output of Ruby code, which is contained in the database and may be modified at runtime. After that, I detail how to create inheritance hierarchies so that if a property does not exist for a given object, its parents can be checked for a given property.

Setting and Retrieving Values

Listing One lets you set and and retrieve values from a database. (Listing Four, which we'll get to shortly, is the SQL script for creating a test database to run the Ruby examples I present here.)

Listing One
require 'config.rb'
require 'mysql'
$db = connect_db()
class Test_Object
    @@properties = ['property_one','property_two']
    MYTABLE = 'test_objects'
    attr_reader :object_id
    def initialize(object_id)
        @object_id=object_id
    end
    def method_missing( name, *args )
        method_name = name.to_s
          if (method_name.slice(-1,1) == '=')
            field_name = method_name.chop
            set field_name,args[0]
          else
            field_name=method_name
            if @@properties.include?(field_name)
            res= $db.query("SELECT #{field_name} FROM #{MYTABLE} 
                                     where object_id='#{@object_id}';")
            row = res.fetch_row
            if (res.num_rows()==0) or row.nil?
                nil
            else    
                row[0]
            end
             end
          end
    end
    def set( name, value )
        if @@properties.include?(field_name)
          $db.query("UPDATE #{MYTABLE} set #{name}=
            \"#{Mysql.escape_string(value)}\" where object_id=#{@object_id};")
        end
    end
end
#run some tests
test_object = Test_Object.new(1)
print "Testing property: #{test_object.property_one}\n"

Listing One defines a Test_Object class, along with two class-scope private members:

It also defines a method to read object_id, as well as an initialize method that lets you call Test_Object.new() with an object_id as a parameter.

The method_missing method is executed when a nonexistent method is called. In Ruby, an assignment to a public member of a classic is simply a method whose name is the property name followed by an equals sign ("="). Because of this, the routine first checks if the rightmost character of the string is an equals sign. If it is, it's an assignment, so it calls the set method to set the field appropriately. The set method is a method of the class that uses an UPDATE statement to set the field equal to the new value. If the first character is not an equals sign, it checks if the method is in the list of the fields you are familiar with. If it is, then it retrieves the value and returns it.

This technique has its downside. As implemented, it queries the database with a SQL UPDATE statement whenever an assignment is made. This can result in inefficiency when you make a large number of assignments. This may be an issue in situations where UPDATE statements are expensive or a large number of fields are modified at one time. You may wish to modify the technique so that the statements are stored until some update method is called that makes all the assignments at once. Of course, this reduces the intuitiveness and elegance of the solution, possibly inducing bugs. The other possibilities include a separate method for making updates to a number of properties at once, such as a separate method to set multiple properties, or perhaps a begin_batch_update and end_batch update method, which executes all the assignments inside the block once the block ends.

To this point, this technique is not complicated, similar to how you might implement it in most object-oriented languages. However, the strength of the technique is that the code is very orthogonal, and there isn't duplication of code for each property you add. Instead, you can add additional properties just by adding new elements to @@properties. You can also add additional functionality—type checking, auditing, compression, encryption, and the like— easily and in a central place. The lack of code duplication, as well as the lack of special syntax, makes this technique stand out. Still, the really interesting part comes next.

Methods Stored In the Database

Listing Two defines a class that extends the technique. It contains the same functionality as the first class, with some additions. If a method is called that does not exist, and it is not part of the @@properties array, it is assumed that it is part of a second, dynamic table of properties for the object. The name of this table is defined as a class-scope constant, EXTENDED_PROPERTIES_TABLE. This table contains one property for one object per row, and each property can have either a static value or code associated with it.

Listing Two
require 'config.rb'
require 'mysql'
$db = connect_db()
class Test_Object
    @@properties = ['property_one','property_two']
    MYTABLE = 'test_objects'
    EXTENDED_PROPERTIES_TABLE = 'test_objects_props'
    attr_reader :object_id
    def initialize(object_id)
        @object_id=object_id
    end
    def method_missing( name, *args )
        method_name = name.to_s
          if (method_name.slice(-1,1) == '=')
            field_name = method_name.chop
            set field_name,args[0]
          else
            field_name=method_name
            if @@properties.include?(field_name)
            res= $db.query("SELECT #{field_name} FROM #{MYTABLE} 
                                     where object_id='#{@object_id}';")
            row = res.fetch_row
            if (res.num_rows()==0) or row.nil?
                nil
            else    
                row[0]
            end
              else
            get_property name   
             end
          end
    end
    def get_property(name)
        res= $db.query("select `code`,`value` 
             from #{EXTENDED_PROPERTIES_TABLE} 
              where object_id='#{@object_id}' and `property_name`='#{name}';")
        code,value = *res.fetch_row()
        if res.num_rows()==0
          nil
        else 
            if code.nil?
                if value.nil?
                    nil
                else
                    value
                end
            else
                begin
                    eval code
                    rescue Exception
                      if @args.nil?
                          raise Exception.exception("Error in evaluating for 
                            object ##{@object_id} in method #{name} 
                             with no arguments. Error description: \"#$!\"")
                      else
                          raise Exception.exception("Error in evaluating for 
                            object ##{@object_id} in method #{name} 
                                with arguments #{@args.join(",")}. 
                                    Error description: \"#$!\" ")
                      end
                end
                
                end
        end
    end
    def set( name, value )
        if @@properties.include?(field_name)
          $db.query("UPDATE #{MYTABLE} set #{name}=\
            "#{Mysql.escape_string(value)}\" where object_id=#{@object_id};")
        end
    end
end
#run some tests
test_object = Test_Object.new(1)
print "Extended property: #{test_object.test_extended_property}\n"
print "Extended method: #{test_object.test_extended_method}\n"

When a property is not part of the first table, then it is assumed to be an extended property, part of the extended properties table. These extended properties are different from the properties part of the first table; they may contain executable code instead of values. Because of that, they are more flexible; a static value may be changed to a dynamic value at runtime. Additionally, because there is one property per row, new properties may be added to an object at runtime without modifying the structure of the main object table.

How are extended properties evaluated? First, the extended properties table is checked for a row with the appropriate object ID and property name. If it's not found—in other words, if someone called a method that simply didn't exist—then it returns nil, although in many situations it is preferable to raise an error. Next, it checks if there is code associated with that method. If there is, then it evaluates that code using Ruby's built-in evaluation faculty.

Ruby runs the code in the current context so that the self object refers to the correct object, and the code can access private class members. Additionally, the code can access local variables from the calling method, the get_property function. One of those local variables is the args array—the array containing the arguments passed to the method. Since the evaluated code has access to that array, it can simply take its arguments from the array. If so desired, you can check that the correct number of arguments is passed to the method, either inside the method itself or by adding an additional field to the extended properties table. No type checking is done for the arguments. Since Ruby itself does not perform argument type checking, that probably won't be a problem, although you could modify the get_property code to ensure that each argument was of the proper type.

However, properties that do not need the capacity to contain code should be part of the objects table wherever possible. Why? To perform a query on extended properties, it requires at least one additional subquery for each property to be examined. This can be a severe performance problem if not used correctly. This is not an issue when updating or querying a single object, however, and so this can be an extremely powerful technique when the property is not frequently used as part of large queries.

Implementation Inheritance

Listing Three defines a third revision of the Test_Object class—one that supports implementation inheritance entirely within the database, essentially by causing undefined method calls on an object to be handed to its parent object. An additional property, parent, is added to the @@properties array and to the test_objects table. The parent property specifies the ID of the parent Test_Object. The get_property method is modified so that if a property can't be found, it calls the get_property method of the parent object. This continues until a property is found or until the parent property is nil (which is how Ruby's MySQL interface represents SQL's NULL).

Listing Three
require 'config.rb'
require 'mysql'
$db = connect_db()
class Test_Object
    @@properties = ['parent','property_one','property_two']
    MYTABLE = 'test_objects'
    EXTENDED_PROPERTIES_TABLE = 'test_objects_props'
    attr_reader :object_id
    def initialize(object_id)
        @object_id=object_id
    end
    def method_missing( name, *args )
        method_name = name.to_s
          if (method_name.slice(-1,1) == '=')
            field_name = method_name.chop
            set field_name,args[0]
          else
            field_name=method_name
            if @@properties.include?(field_name)
            res= $db.query("SELECT #{field_name} FROM #{MYTABLE} 
                               where object_id='#{@object_id}';")
            row = res.fetch_row
            if (res.num_rows()==0) or row.nil?
                nil
            else    
                row[0]
            end
              else
            get_property name   
             end
          end
    end
    def get_property(name)
        res= $db.query("select `code`,`value` 
             from #{EXTENDED_PROPERTIES_TABLE} 
              where object_id='#{@object_id}' and `property_name`='#{name}';")
        code,value = *res.fetch_row()
        if res.num_rows()==0
          if not parent.nil?
            Test_Object.new(parent).get_property name
          else
            nil
          end
        else 
            if code.nil?
                if value.nil?
                    nil
                else
                    value
                end

           else
                    eval code 
                end
        end
    end

    def set( name, value )
        if @@properties.include?(field_name)
          $db.query("UPDATE #{MYTABLE} set #{name}=\
            "#{Mysql.escape_string(value)}\" where object_id=#{@object_id};")
        end
    end
end
#run some tests
test_object = Test_Object.new(1)
print "Extended property: #{test_object.test_extended_property}\n"
print "Extended method: #{test_object.test_extended_method}\n"
print "Inherited property: #{test_object.test_inherited_property}\n"

If desired, multiple inheritance can be implemented in a similar manner—Ruby's flexibility and this technique makes it relatively straightforward.

However, as presented, the code does not check for infinite loops. If object 2 has object 1 as a parent, and object 1 has object 2 as a parent, and either object has an undefined method call, the code continues to search until a stack overflow. It's straightforward to implement this.

Debugging

There are a few considerations regarding debugging. First, what is going to happen when there is an error in code executed from the database? You may wish to raise the error so the calling method must handle it, as our example does, or deal with it otherwise: log the error, simply return a nil, print a message to the console, or some other action. This example raises the error and adds some additional information—the fact that the error occurred in code inside the database, the method name, the object id, and the arguments passed to the method, if any.

The other decision to be made is what to do in the event of a method call that isn't found even if the extended properties table is checked. The example I present (Listing Four) simply returns nil; it may be preferable to raise an "undefined local variable or method" error instead.

Listing Four
# Use this SQL script to create a test database to run the examples. 
# phpMyAdmin MySQL-Dump
# version 2.2.7-pl1
# http://phpwizard.net/phpMyAdmin/
# http://www.phpmyadmin.net/ (download page)
# Host: localhost
# Generation Time: Apr 10, 2004 at 10:09 AM
# Server version: 4.00.18
# PHP Version: 4.3.1
# Database : `test_database`
# --------------------------------------------------------

CREATE database test_database; 
USE test_database;

# Table structure for table `test_objects`
CREATE TABLE test_objects (
  object_id int(11) NOT NULL,
  property_one text NOT NULL,
  property_two text NOT NULL,
  parent int(11) NULL,
  PRIMARY KEY  (object_id)
) TYPE=MyISAM;

# Dumping data for table `test_objects`
INSERT INTO test_objects (object_id, property_one, property_two, parent) 
                                VALUES (0, 'parent of object 1', '', null);
INSERT INTO test_objects (object_id, property_one, property_two, parent) 
                             VALUES (1, 'property_one', 'property_two', 0);
# --------------------------------------------------------
# Table structure for table `test_props`

CREATE TABLE test_objects_props (
  property_name text NOT NULL,
  object_id int(11) NOT NULL,
  value text,
  code text
) TYPE=MyISAM;
# Dumping data for table `test_objects_props`
INSERT INTO test_objects_props (property_name, object_id, value, code) 
                VALUES ('test_extended_property', 1, 'test suceeded', NULL);
INSERT INTO test_objects_props (property_name, object_id, value, code) 
                  VALUES ('test_extended_method', 1, NULL, 'Kernel.rand()');
INSERT INTO test_objects_props (property_name, object_id, value, code) 
                  VALUES ('test_inherited_property', 0, 
                            'testing the inherited property suceeded', NULL);

Typical Use

These techniques are useful in a variety of applications. Of course, it is really only effective in situations where object-oriented design is effective—which varies depending on who you ask. The three methods I've discussed here:

are all appropriate in differing circumstances.

Runtime modification of the code may not be useful in your project, in which case you may wish only to take advantage of the method_missing technique. Alternatively, you may wish to begin with a more traditional class, and then gradually move some methods from the file-bound Ruby source to the database—the transition should be fairly smooth and relatively painless.

Also note that while this example runs code from a database, other methods are available. It's also possible to read code or code fragments from a text file — for example, a configuration file (see Listing Five) — and then use the method_missing technique to run that as part of the class. This can be extended, as well, to involve nonRuby code; for example, you may want to have properties that are defined as the result of a calculation involving other properties of the object. You can store the formulas in either a text file or a database, and then either parse them or convert them to Ruby code. In that way, for example, you can easily change business formulas without source changes — and it's still elegant and orthogonal.

Listing Five
require 'mysql';

$dbhost="localhost"
$dbname="test_database"
$dbuser="root"
$dbpasswd=""
$port = 7232

def connect_db
    Mysql.new($dbhost, $dbuser, $dbpasswd,$dbname)
end

Conclusion

While Ruby certainly does not have the performance of C++, it has much of the power of Perl, and its orthogonality gives it better maintainability for larger projects. This is especially evident in the ability it gives you to extend the language. Such extensions must be done carefully—in some cases, the performance problems can be significant. Even so, the techniques presented here can be a significant boon to a Ruby software project.


David is a consultant based in New Hampshire. He can be contacted at berubeconsulting.com.

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.