NAME
Storm::Tutorial - Getting started with Storm
DESCRIPTION
Storm is a Moose based library for storing and retrieving Moose based objects using a DBI connection.
CONNECTING
Storm connects to databases using the uqiquitous DBI module. Database handles are spawned using a Storm::Source object which holds connection information. In the example below, the Storm::Source object is coerced from the arguments passed to the Storm constructor.
use Storm;
$storm->new(
source => ['DBI:mysql:timsdev:10.0.1.11:3306', 'user', 'pass']
);
BUILDING
Storm is for storing Moose based objects. It is required that the objects have the Storm roles and meta-roles applied.
Storm::Builder is an extension of Moose which applies the appropriate roles and meta-roles, as well as providing some sugar for defining relationships.
Simple example
package Person;
use Storm::Builder;
__PACKAGE__->meta->table( 'Person' );
has 'id' => (
is => 'rw',
traits => [qw(PrimaryKey AutoIncrement)],
);
has 'name' => (
is => 'rw',
);
has 'dob' => (
is => 'rw',
column => 'birth_date',
);
This is a very simple example, but demonstrates a few key concepts:
Every class definition must have a meta-table defined. Storm uses this information to determine what table to store the object to in the database.
It is recomended that every class provide a primary key (via the PrimaryKey attribute trait.) If you do not provide a primary key, you will not be able to use lookup queries to restore objects, nor will any other Storm enabled object be able to store references to it.
Storm avoids requiring a separate schema by defining elements of a schema in the object definition. By default, object attributes are assigned a table column with the same name as the attribute. The default behavior can be changed by setting the column option.
Circular references
package Person;
use Storm::Builder;
__PACKAGE__->meta->table( 'People' );
has 'id' => (
is => 'rw',
traits => [qw(PrimaryKey AutoIncrement)],
);
has 'spouse' => (
is => 'rw',
isa => 'Person',
weak_ref => 1,
);
References to other Storm enabled classes are serialized automatically using the primary key. This is accomplished by setting the
isa
option to a Storm enabled class (type).In a scenario such as this, where two objects will reference each other in a circular structure, it is necessary to set the weak_ref option to avoid memory leaks. When constructing and using objects with circular references, it is necessary to manage the
scope
. The scope stops objects from being garbage collected to early (i.e. when the only references to them are weak.)
RELATIONSHIPS
Relationships are devised in two ways. We demonstrated one manner in the example above by setting an attributes isa
option to a Storm enabled class. This allows you to referance a singular object. Here we will demonstrate making one-to-many
and many-to-many
relationships using the has_many
keyword.
One-to-many
package Person;
use Storm::Builder;
__PACKAGE__->meta->table( 'People' );
has 'id' => (
is => 'rw',
traits => [qw(PrimaryKey AutoIncrement)],
);
has_many 'pokemon' => (
foreign_class => 'Pokemon',
foreign_key => 'master',
handles => {
pokemon => 'iter',
}
);
package Pokemon;
use Storm::Builder;
__PACKAGE__->meta->table( 'Pokemon' );
has 'id' => (
is => 'rw',
traits => [qw(PrimaryKey AutoIncrement)],
);
has 'master' => (
is => 'rw',
isa => 'Person',
weak_ref => 1,
);
Here, we define the components of a relationship between the Person class and the Pokemon class uing the has_many
keyword.
The
foreign_key => master
denotes that the relationship is made by matching the primary key of the Person with the c<master> attribute of the Pokemon.Using the
handles
option, we create thepokemon
method for Person. This method returns a Person's Pokemon in the form of a Storm::Query::Select::Iterator object.To add another Pokemon to a Person, create a new Pokemon and set the
master
attribute to a$person
.
Many-to-many
package Person;
use Storm::Builder;
__PACKAGE__->meta->table( 'People' );
has 'id' => (
is => 'rw',
traits => [qw(PrimaryKey AutoIncrement)],
);
has_many 'pets' => (
is => 'rw',
foreign_class => 'Pets',
junction_table => 'PeoplesPets',
local_match => 'person',
foreign_match => 'pet',
handles => {
parents => 'iter',
add_pet => 'add',
remove_pet => 'remove',
}
)
package Pet;
use Storm::Builder;
__PACKAGE__->meta->table( 'Pets' );
has 'id' => (
is => 'rw',
traits => [qw(PrimaryKey AutoIncrement)],
);
has_many 'care_takers' => (
is => 'rw',
foreign_class => 'Pets',
junction_table => 'PeoplesPets',
local_match => 'person',
foreign_match => 'pets',
handles => {
care_takers => 'iter',
add_care_taker => 'add',
remove_care_taker => 'remove',
}
)
In a many-to-many relationship, a junction_table is required to form the relationship. This is specified as an option to the
has_many
keyword.We also need to define the columns in the junction_table that will be used to identify the components of the relationship. This is done with local_match and foreign_match options. local_match is the column in the junction table to match with the primary key of defining class, while foreign_match is the the column to match with the primary key of the foregin class.
Using the
handles
option, we create methods for retrieving a Storm::Query::Select::Iterator, as well as methods for adding and removing pets/caretakers.$pet->add_care_taker( $person )
is synanamous with$person->add_pet( $pet )
and$pet->remove_care_taker( $person )
is synanamous with$person->remove_pet( $pet )
.
CRUD
Storm provides queries for the four basic data operations Create, Read, Update, and Delete (CRUD) as well as a select
query for searching.
Insert
$storm->insert( @objects );
Inserts @objects into the database. Objects may onle be inserted if they do not already exist in the database. An error will be thrown if you try to insert an object that already exists. An error will also be thrown if the object has a primary key and it is undef (unless using the AutoIncrement trait.)
Lookup
$storm->lookup( $class, @object_ids );
Retrieves object from the database by primary key. The $class
attribute is required so Storm knows where to find and how to inflate the objects. If any of the object's attributes reference other Storm enabled objects, they will be looked up and inflated as well. This will continue until all dependent object have been retrieved and inflated.
Update
$storm->update( $class, @objects );
Updates the state of the @objects in the database. If you try to call update
on an object that is not already in the database, an error will be thrown. Only the @objects passed to update
will be affected, any Storm enabled objects they reference will not updated in the database. You must call update
on them yourself.
Delete
$storm->delete( $class, @objects );
Deletes the @objects from the database. The local references to them will still exists until you destroy them or they go out of scope.
SELECT
Searching is possible using a select query. The select query is a little more complex than it's counterparts.
Iterators
$query = $storm->select( 'Person' );
$iter = $query->results;
while ( $object = $iter->next ) {
... do stuff with $object ...
}
Calling the results
method on a select query returns a Storm::Query::Select::Iterator for iterating over the result set.
Where
$query = $storm->select( 'Person' );
$query->where( '.last_name', '=', 'Simpson' );
$query->where( '.age', '>', 10 );
$iter = $query->results;
Use Storm::Query::Select's where
method to select specific objects.
The following comparisons are supported: =, <>, <, <=, =>, IN, NOT IN, BETWEEN, LIKE, NOT LIKE
It is possible to use attributes in a comparison with the
.attribute
notation (to distinguish them from regular strings.)$query->where( '.spouse.first_name', '=', 'Marge' );
If the attribute is also a Storm enabled object you can can reference it's attributes in the comparison as well.
Order-by
$query->order_by( '.lastname', '.age DESC' );
Use the order_by
method to sort the results.
SCOPE
The scope ensures that objects aren't garbage collected to early. As objects are inflated from the database, the are pushed onto the live object scope, increasing their reference count.
Let's define out person class to use as an example.
package Person;
use Storm::Builder;
has 'id' => (
is => 'rw',
traits => [qw(PrimaryKey AutoIncrement)],
);
has 'name' => (
is => 'rw',
);
has 'spouse' => (
is => 'rw',
isa => 'Person',
weak_ref => 1,
);
Now, insert some objects into the database.
$storm->insert(
Person->new( name = 'Homer' ),
Person->new( name = 'Marge' )
);
And then we can link them together:
{
my $scope = $storm->new_scope;
my ( $homer, $marge ) = $storm->lookup( $homer_id, $marge_id );
$homer->spouse( $marge );
$marge->spouse( $homer );
$storm->update( $homer, $marge );
}
Now we can we can load the objects from the database like this:
{
my $scope = $storm->new_scope;
my $homer = $storm->lookup( $homer_id );
print $homer->spouse->name; # Marge
}
{
my $scope = $storm->new_scope;
my $marge = $storm->lookup( $marge_id );
print $marge->spouse->name; # Homer Simpson
refaddr( $marge ) == refaddr( $marge->spouse->spouse ); # true
}
When the initial object is loaded, all the objects that the initial object depends on will be loaded. This will continue until all dependent objects have been inflated from the database.
If we did not use a scope, by the time $homer his spouse attribute would have been cleared because there is no other reference to Marge. Here is a code snippet that demonstrates why:
sub get_homer {
my $homer = Person->new( name => 'Homer' );
my $marge = Person->new( name => 'Marge' );
$homer->spouse( $marge );
$marge->spouse( $homer );
return $homer;
# at this point $homer and $marge go out of scope
# $homer has a refcount of 1 because it's the return value
# $marge has a refcount of 0, and gets destroyed
# the weak reference in $homer->spouse is cleared
}
my $homer = get_homer();
$homer->spouse; # this returns undef
By using this idiom:
{
my $scope = Storm->new_scope;
... do all Storm work in here ...
}
You are ensuring that the objects live at least as long as necessary.
In a web application context, you usually create one new scope per request.
Credit
The live object scope was largely inspired by the KiokuDB module. Some of the code and documentation for this functionality was used under license directly from the KiokuDB source.
TRANSACTIONS
When using a supporting databse, you can use the do_transaction
method to execute a code block and commit the transaction.
eval {
$storm->do_transaction( sub {
... do work on $storm ...
});
}
print $@ if $@; # prints error
The transaction will only be committed if they block executes successfully. If any exceptions are thrown, the transaction will be rolled back. It is recommended that you execute the transaction inside an eval block to trap any errors that are thrown. Alternatively, you can use a module like TryCatch or Try::Tiny to trap errors.
POLICY
The policy is used to determine what data type is used by the DBMS to store a value. The policy also determines how different types of values are inflated/deflated.
package My::Policy;
use Storm::Policy;
define 'DateTime', 'DATETIME';
transform 'DateTime',
inflate { DateTime::Form::SQLite->parse_datetime( $_ ) },
deflate { DateTime::Form::SQLite->format_datetime( $_ ) };
package main;
use Storm;
$storm->new( source => ..., policy => 'My::Policy' );
define
-
Use the
define
keyword to determine what data type the DBMS should used to store a value of the given type. In this case we want DateTime objects to be stored in the database using theDATETIME
data type. transform
-
Use the
transform
keyword for setting a custom inflator/deflator for a type.The inflator is defined using the
inflate
keyword. The$_
special variable will be set to the value to be inflated. The inflator is expected to return the inflated value.The deflator is defined using the
deflate
keyword. The$_
special variable will be set to the value to be deflated. The deflator is expected to return the deflated value.
AEOLUS
Aeolus is the greek god of the wind. Aeolus helps manage your database installation. With Aeolus you can easily install and remove the tables your classes need to store their data.
$storm->aeolus->install_class( 'Person' );
See Storm::Aeolus for more information.
AUTHOR
Jeffrey Ray Hallock <jeffrey.hallock at gmail dot com>
COPYRIGHT
Copyright (c) 2010 Jeffrey Ray Hallock. All rights reserved.
This program is free software; you can redistribute it and/or
modify it under the same terms as Perl itself.