Creating a Custom Datatype in Rose::DB

While Rose::DB is very robust and almost all data-types are already provided, there are some instances where a custom data-type is required. This document outlines the steps that are required to adding a custom data-type to be used in a Rose::DB::Object. The examples that are shown are for adding the earthdistance data-type that is provided by the PostgreSQL database contrib packages. Visit this page for more details about this data-type.

NOTE: This document is more of an example with notes than a traditional HowTo document.

Contents

Adding the MakeMethods Class

Before a column object can be created or used Rose::DB needs to know how to make the accessor methods for this data-type. This is done by creating a new Rose::DB::Object::MakeMethods object. For this example since the earthdistance data-type is PostgreSQL specific we will use the class name of Rose::DB::Object::MakeMethods::Pg::Earth.

This new class must include the method maker method and should be named something reasonable for the data-type it is going to represent. However it can be called anything desired, when the column is defined later the class and method name are both provided. In this instance the method name chosen is earth.

The basic class definitions for a new MakeMethods subclass would look similar to this:

package Rose::DB::Object::MakeMethods::Pg::Earth;

use strict; 
use warnings;

use base qw(Rose::Object::MakeMethods);

# Import constants to be used in the earth() method
use Rose::DB::Object::Constants qw(PRIVATE_PREFIX FLAG_DB_IS_PRIVATE
    STATE_IN_DB STATE_LOADING STATE_SAVING ON_SAVE_ATTR_NAME MODIFIED_COLUMNS
    SET_COLUMNS);

sub earth {  
    ...
}

The next step is to define the methods, or interfaces, that can be used to access a column with this data-type. This is done in our MakeMethods method maker (earth()). This method takes two main arguments in addition to the standard $package argument. The first argument is the method name by which the returned code refernece will be called, for instance if the $name value is set to our_location, then later when an instance of this column is accessed you can call $column->our_location(). The second argument is a hash reference containing additional arguments to be used in the method making process. For a full list of all the arguments that can be passed see the Rose::DB::Object::MakeMethods documentation. The following is a table of the keys and values that are used in this example.

Key Value
hash_key The key inside the hash-based object to use for the storage of this attribute. Defaults to the name of the method.
interface Which interface method is being created. When the maker method is called it is called once for each type of method that is to be added to the column. For example once for each of get, set, get_set, etc methods.
smart_modification Indicates if we want to use smart_modification of columns. If we are then we should indicate when a column has been updated to the object so that it will know it needs to update the database.
_method_type The type of method that is being created. This allows you to use one maker method for multiple method types and then conditionally switch behavior based on which method type is actually being created.
column A link to the Rose::DB::Object::Metadata::Column object for which this method is being called on to create the methods.

The goal of the method maker method is to create a code reference that will acheive the desired effect for the interface given. This code reference must be returned in a hash reference with the key of the hash reference being the desired method_name that was provided as the $name arugment. For example if we have asked the method maker to make a get method for the earthdistance data-type and we want to call this accessor method earth we would need to return a hash reference like this:

return {
    'earth' => sub {
        my $self = shift;
        return(@{$self->{"Column_Name"}});
    }
}

Since each method needs to have its own subroutine defined and returned this is done in an if-block so each interface type can return its own code reference. The finished example for the earth method maker would look like this:

sub earth {
   my ($class, $name, $args) = @_; 

   my $key         = $args->{'hash_key'} || $name;
   my $interface   = $args->{'interface'} || 'get_set';
   my $smart       = $args->{'smart_modification'};
   my $type        = $args->{'_method_type'} || 'earth';
   my $column_name = $args->{'column'} ? $args->{'column'}->name : $name;

   my $qkey = $key;
   $qkey =~ s/'/\\'/g;

   my $qname = $name;
   $qname =~ s/"/\\"/g;

   my $col_name_escaped = $column_name;
   $col_name_escaped =~ s/'/\\'/g;

   my %methods;
   if($interface eq 'get_set') {
      $methods{$name} = sub {
         my ($self, @args) = @_;

         my $value;
         if (@args > 0) {
            if (ref($args[0]) eq 'ARRAY') {
               $value = $args[0];
            }
            elsif (ref($args[0]) eq 'HASH') {
               my $lat = exists($args[0]->{latitude}) ? $args[0]->{latitude} : 
                            exists($args[0]->{lat}) ? $args[0]->{lat} : undef;
               my $long = exists($args[0]->{longitude}) ? $args[0]->{longitude} :
                            exists($args[0]->{long}) ? $args[0]->{long} : undef;
               $value = [$lat, $long];
            }
            elsif (@args > 1) {
               $value = [$args[0], $args[1]];
            }
            elsif ($args[0] =~ /{(-?\d+(\.\d+)?)\s*,\s*(-?\d+(\.\d+)?)}/) {
               $value =[$1, $3];
            }

            no warnings;
            $self->{MODIFIED_COLUMNS()}{$col_name_escaped} = 1 
                    unless($self->{STATE_LOADING()});
            $self->{SET_COLUMNS()}{$col_name_escaped} = 1 if ($smart);

            $self->{$qkey} = $value;
         }

         return(@{$self->{$qkey}});

      }
   }
   elsif ($interface eq 'get') {
      $methods{$name} = sub {
         my $self = shift;

         return(@{$self->{"$qkey"}});
      }
   }
   elsif($interface eq 'set') {
      $methods{$name} = sub {
         my ($self, @args) = @_;

         Carp::croak ref($self), ": Missing argument in call to $qname" 
                unless(@args > 1);
         my $value;
         if (ref($args[0])) {
            $value = $args[0];
         }
         elsif (@args > 1) {
            $value = [$args[0], $args[1]];
         }

         no warnings;
         my $old_val = $self->{'$qkey'};
         $self->{$qkey} = $value;

         unless($self->{STATE_LOADING()} || !defined($old_val) ||
               ($old_val->[0] != $value->[0] && $old_val->[1] != $value->[1])) {
            $self->{MODIFIED_COLUMNS()}{$col_name_escaped} = 1;
         }
         $self->{SET_COLUMNS()}{$col_name_escaped} = 1 if ($smart);

         return(@{$self->{$qkey}});
      }
   }
   elsif ($interface eq 'longitude') {
      $methods{$name} = sub {
         my $self = shift;

         my $coords = $self->{$qkey};
         return(undef) if (!ref($coords) eq 'ARRAY');

         return($coords->[0]);
      }
   }
   elsif ($interface eq 'latitude') {
      $methods{$name} = sub {
         my $self = shift;

         my $coords = $self->{$qkey};
         return(undef) if (!ref($coords) eq 'ARRAY');

         return($coords->[1]);
      }
   }
   else {
      Carp::croak "Unknown interface: $interface"
   }

   return \%methods;
}

Adding the Column Object

With the new method maker now available we can define a new column data-type that will use these methods. This is done by creating a customized Rose::DB::Object::Metadata::Column object. Again as the earthdistance data-type is PostgreSQL specific we will define our column object as Rose::DB::Object::Metadata::Column::Pg::Earth.

When creating this object we need to indicate the additional method names that need to be made for this data-type. This is done with the following call:

__PACKAGE__->add_common_method_maker_argument_names(
    qw(latitude longitude)
);

This tells Rose to include the latitude and logitued methods when making the accessor methods on colums for this data-type.

Rose then needs to be told which MakeMethods class and method to use when creating the required accessor methods. This is done by running the following loop:

foreach my $type (__PACKAGE__->available_method_types)
{
   __PACKAGE__->method_maker_class(
        $type => 'Rose::DB::Object::MakeMethods::Pg::Earth'
    );
   __PACKAGE__->method_maker_type($type => 'earth');
}

Finally we want to provide the information on how to construct the queries that will be used when access this type of data in the database. We can do this be defining our own query place holder methods. A separate method is available for querying, inserting and updating. In this case the query string is the same for querying, inserting or updating so we only have to define one of these methods and then map the other two methods to use the same code reference.

sub select_sql
{
   my $column = shift->SUPER::select_sql(@_);
   return($column) if (!defined($column));
   return("ARRAY[latitude($column), longitude($column)]");
}

*insert_placeholder_sql = \&query_placeholder_sql;
*update_placeholder_sql = \&query_placeholder_sql;

Putting this all together gives us the following class definition for Rose::DB::Object::Metadata::Column::Pg::Earth.

package Rose::DB::Object::Metadata::Column::Pg::Earth;
use strict;
use warnings;

use Rose::Object::MakeMethods::Generic;
use Rose::DB::Object::MakeMethods::Pg::Earth;
use Rose::DB::Object::QueryBuilder;

use base qw(Rose::DB::Object::Metadata::Column);

# Tell Rose to also define a latitude and longitude method.
__PACKAGE__->add_common_method_maker_argument_names(
   qw(latitude longitude)
);

Rose::Object::MakeMethods::Generic->make_methods (
  { preserve_existing => 1 },
  scalar => [ __PACKAGE__->common_method_maker_argument_names ]
);

# Build the methods using our custom Earth class.
foreach my $type (__PACKAGE__->available_method_types)
{
   __PACKAGE__->method_maker_class(
        $type => 'Rose::DB::Object::MakeMethods::Pg::Earth'
    );
   __PACKAGE__->method_maker_type($type => 'earth');
}

# Indicate that this is a column of type earth.
sub type { return 'earth' }

# Instead of using query place holders we want to include the value in-line in
# queries.
sub should_inline_value {
   return 1;
}

sub query_placeholder_sql {
   my ($self) = @_;

   return('ll_to_earth(?, ?)');
}

sub select_sql {
   my $column = shift->SUPER::select_sql(@_);
   return($column) if (!defined($column));
   return("ARRAY[latitude($column), longitude($column)]");
}

# Just map the other query placeholder methods to our query placeholder
# method.
*insert_placeholder_sql = \&query_placeholder_sql;
*update_placeholder_sql = \&query_placeholder_sql;

Using the Datatype in a Rose::DB::Object

When you have a Rose::DB::Object that will require the method type you need to tell that object where to find this new column type. This can either be done inside each Rose::DB::Object that uses the type, or you can define this once inside a base class and then subclass the new base class for all of your Rose::DB::Objects instead. Either way would work.

To tell Rose where to find the new object you will need to define the new column type. Also during this you will need to tell Rose what type of data is returned from this column. The following needs to be added to a Rose::DB::Object definition before the new data-type can be used:

use Rose::DB::Object::Metadata::Column::Pg::Earth;
Rose::DB::Object::Metadata->_column_type_class( 
    earth => 'Rose::DB::Object::Metadata::Column::Pg::Earth' 
);
*Rose::DB::Object::MakeMethods::Generic::earth = sub {
    my($class, $name, $args) = @_;
    $args->{'_method_type'} = 'scalar';
    $class->scalar($name, $args);
};

Once done you are then able to use the earthdistance column type inside your Rose::DB::Objects. Here is a short example of accessing a Rose::DB::Object with the new data-type

package Location;
use base qw(My::DB::Object);

__PACKAGE__->meta->setup(
   table   => 'location',
   columns => [
      name =>      { type => 'text', not_null => 1 },
      location =>  { type => 'earth', not_null => 1 },
   ],
);

package Example;

my $l = Location->new(
    name => 'Location Name',
    location => [$lat, $long]);

print "Latitude" . $l->latitude . "\n";
print "Longitude" . $l->longitude . "\n";