NAME
DBIx::SQLEngine - Extends DBI with High-Level Operations
ABSTRACT
The DBIx::SQLEngine class provides an extended interface for the DBI database framework. Each SQLEngine object is a wrapper around a DBI database handle, adding methods that support ad-hoc SQL generation and query execution in a single call. Dynamic subclassing based on database server type enables cross-platform portability. An object mapping layer provides classes for tables, columns, and records.
SYNOPSIS
DBI Wrapper: Adds methods to a DBI database handle.
$sqldb = DBIx::SQLEngine->new( $dbi_dsn, $dbi_user, $dbi_passwd );
$sqldb = DBIx::SQLEngine->new( $dbh ); # or use your existing handle
$dbh = $sqldb->get_dbh(); # get the wraped DBI dbh
$sth = $sqldb->prepare($statement); # or just call any dbh method
High-Level Interface: Prepare and fetch in one call.
$row_count = $sqldb->try_query($sql, \@params, 'get_execute_rowcount');
$array_ary = $sqldb->try_query($sql, \@params, 'fetchall_arrayref');
$hash_ary = $sqldb->try_query($sql, \@params, 'fetchall_hashref');
Data-Driven SQL: SQL generation with flexible arguments.
$hash_ary = $sqldb->fetch_select(
table => 'students', where => { 'status'=>'minor' },
);
$sqldb->do_insert(
table => 'students',
values => { 'name'=>'Dave', 'age'=>'19', 'status'=>'minor' },
);
$sqldb->do_update(
table => 'students', where => 'age > 20',
values => { 'status'=>'adult' },
);
$sqldb->do_delete(
table => 'students', where => { 'name'=>'Dave' },
);
Named Definitions: Pre-define connections and queries.
DBIx::SQLEngine->define_named_connections(
'test' => 'dbi:AnyData:test',
'production' => [ 'dbi:Mysql:our_data:dbhost', 'user', 'passwd' ],
);
DBIx::SQLEngine->define_named_queries(
'all_students' => 'select * from students',
'delete_student' => [ 'delete * from students where id = ?', \$1 ],
);
$sqldb = DBIx::SQLEngine->new( 'test' );
$hash_ary = $sqldb->fetch_named_query( 'all_students' );
$rowcount = $sqldb->do_named_query( 'delete_student', $my_id );
Portability Subclasses: Uses driver's idioms or emulation.
$hash_ary = $sqldb->fetch_select( # uses database's limit syntax
table => 'students', order => 'last_name, first_name',
limit => 20, offset => 100,
);
$hash_ary = $sqldb->fetch_select( # use "join on" or merge with "where"
table => ['students'=>{'students.id'=>\'grades.student'}=>'grades'],
where => { 'academic_year'=>'2004' },
);
$hash_ary = $sqldb->fetch_select( # combines multiple query results
union => [ { table=>'students', columns=>'first_name, last_name' },
{ table=>'staff', columns=>'name_f, name_l' } ],
);
$sqldb->do_insert( # use auto_increment/sequence column
table => 'students', sequence => 'id',
values => { 'name'=>'Dave', 'age'=>'19', 'status'=>'minor' },
);
Object Mapping: Classes for tables, columns, and records.
$table = $sqldb->table('grades');
$hash_ary = $table->fetch_select();
$table->delete_row( $primary_key );
$sqldb->record_class( 'students', 'My::Student' );
@records = My::Student->fetch_select(
where => 'age > 20',
order => 'last_name, first_name',
)->records;
$record = My::Student->new_with_values( 'first_name' => 'Dave' );
$record->insert_record();
$record = My::Student->fetch_record( $primary_key );
print $record->get_values('first_name', 'last_name');
$record->change_values( 'status' => 'adult' );
$record->update_record();
$record->delete_record();
DESCRIPTION
DBIx::SQLEngine is the latest generation of a toolkit used by the authors for several years to develop business data applications. Its goal is to simplify dynamic query execution and to minimize cross-RDMS portability issues.
DBI Wrapper
Each DBIx::SQLEngine::Driver object is implemented as a wrapper around a database handle provided by DBI, the Perl Database Interface.
Arbitrary queries can be executed, bypassing the SQL generation capabilities. The methods whose names end in _sql, like fetch_sql and do_sql, each accept a SQL statement and parameters, pass it to the DBI data source, and return information about the results of the query. Error handling is standardized, and routine annoyances like timed-out connections are retried automatically.
The SQLEngine also allows direct access to the wrapped database handle, enabling use of the entire DBI API for cases when high-level interfaces are insufficient.
Relevant methods are descrbed in "Driver Object Creation" in DBIx::SQLEngine::Driver, "CONNECTION METHODS (DBI DBH)" in DBIx::SQLEngine::Driver, and "STATEMENT METHODS (DBI STH)" in DBIx::SQLEngine::Driver.
High-Level Interface
A combined query interface provides a useful high-level idiom to perform the typical cycle of SQL generation, query execution, and results fetching, all through a single method call.
The various fetch_*, visit_* and do_* methods that don't end in _sql, like fetch_select and do_insert, are wrappers that combine a SQL-generation and a SQL-execution method to provide a simple ways to perform a query in one call.
These methods are defined in "FETCHING DATA (SQL DQL)" in DBIx::SQLEngine::Driver, "EDITING DATA (SQL DML)" in DBIx::SQLEngine::Driver, and "DEFINING STRUCTURES (SQL DDL)" in DBIx::SQLEngine::Driver.
Data-Driven SQL
Several methods are responsible for converting their arguments into commands and placeholder parameters in SQL, the Structured Query Language.
The various methods whose names being with sql_, like sql_select and sql_insert, each accept a hash of arguments and combines then to return a SQL statement and corresponding parameters. Data for each clause of the statement is accepted in multiple formats to facilitate query abstraction, often including various strings, array refs, and hash refs. Each method also supports passing arbitrary queries through using a sql
parameter.
Named Definitions
Connections and queries may be registered in named collections. The named connection feature allows the definition of names for sets of connection parameters, while the named query methods support names for various types of queries in either data-driven or plain-SQL formats.
The definitions may include nested data structures with a special type of placeholders to be replaced by additional values at run-time. References to subroutines can also be registed as definitions, to be called at run-time with any additional values to produce the connection or query arguments.
This functionality is described in "Named Connections" in DBIx::SQLEngine::Driver and "NAMED QUERY CATALOG" in DBIx::SQLEngine::Driver.
Portability Subclasses
Behind the scenes, different subclasses of SQLEngine are instantiated depending on the type of server to which you connect, thanks to DBIx::AnyData.
This release includes subclasses for connections to MySQL, PostgreSQL, Oracle, and Microsoft SQL servers, as well as for the standalone SQLite, AnyData, and CSV packages. For more information about supported drivers, see "Driver Subclasses".
As a result, if you use the data-driven query interface, some range of SQL dialect ideosyncracies can be compensated for. For example, the sql_limit method controls the syntax for select statements with limit and offset clauses, and both MySQL and Oracle override this method to use their local syntax.
However, some features can not be effectively emulated; it's no use to pretend that you're starting a transaction if your database don't have a real atomic rollback/commit function. In those areas, the subclasses provide capability methods that allow callers to determine whether the current driver has the features they require. Features which are only available on a limited number of platforms are listed in "ADVANCED CAPABILITIES" in DBIx::SQLEngine::Driver.
Object Mapping
Built on top of the core SQLEngine functionality is an object mapping layer that provides a variety of classes which serve as an alternate interface to database content.
The Schema classes provide objects for tables and columns which call methods on a SQLEngine to fetch and store data, while the Record classes provide a means of creating subclasses whose instances map to to rows in a particular table using the Schema classes.
Note that this is not a general-purpose "object persistence" system, or even a full-fledged "object-relational mapping" system. It is rather a "relational-object mapping" system: each record class is linked to a single table, each instance to a single row in that table, and each key in the record hash to a value in an identically named column.
Furthermore, no effort has been made to obscure the relational implementation behind the object abstraction; for example, if you don't need the portability provided by the data-driven query interface, you can include arbitrary bits of SQL in the arguments passed to a method that fetch objects from the database.
This functionality is described in "OBJECT MAPPING" and the documentation for the various classes.
ARCHITECTURE
DBIx::SQLEngine is an object-oriented framework containing several class hierarchies grouped into three layers. Applications can use the Driver layer directly, or they can use the Schema or Record layers built on top of it.
Driver Layer
The Driver layer is the primary and lowest-level layer upon which the other layers depend. Each Driver object contains a DBI database handle and is responsible for generating SQL queries, executing them, and returning the results.
Driver objects are wrappers around DBI database handles.
Criteria objects produce elements of SQL where clauses.
See DBIx::SQLEngine::Driver and DBIx::SQLEngine::Criteria.
Schema Layer
The Schema layer centers around the Table object, which combines a Driver object with the name of a table to perform queries against that table. Table objects keep track of their structure as Column objects, and use that information to facilitate common types of queries.
Column objects are very simple structures that hold information about columns in a database table or query result.
ColumnSet objects contain an array of Column objects
Table objects represent database tables accessible via a particular DBIx::SQLEngine.
TableSet objects contain an array of Table objects
See DBIx::SQLEngine::Table and DBIx::SQLEngine::Column, as well as DBIx::SQLEngine::TableSet and DBIx::SQLEngine::ColumnSet.
Record Layer
The Record layer allows you to create Perl classes which are bound to a given Table object. Your Record subclass can fetch rows from the table which will be blessed into that class, and have methods allowing them to be changed and updated back to the database.
Record objects are hashes which represent rows in a Table.
Record Set objects contain an array of Record objects.
See DBIx::SQLEngine::Record::Class and DBIx::SQLEngine::Record::Set.
DRIVER LAYER
This section briefly introduces some of the methods provided by the Driver layer.
Connecting
Create one SQLEngine Driver for each DBI datasource you will use.
- new()
-
DBIx::SQLEngine->new( $dsn, $user, $pass ) : $sqldb DBIx::SQLEngine->new( $dbh ) : $sqldb DBIx::SQLEngine->new( $cnxn_name, @params ) : $sqldb
Creates a Driver object with associated DBI database handle
- define_named_connections()
-
DBIx::SQLEngine->define_named_connections( $name, $cnxn_info )
Defines one or more named connections using the names and definitions provided.
Here's a connection wrapped around an existing DBI database handle.
$dbh = DBI->connect( 'dbi:mysql:livedata', $user, $password ); $sqldb = DBIx::SQLEngine->new( $dbh );
This example shows the use of connection parameters.
$sqldb = DBIx::SQLEngine->new( 'dbi:mysql:livedata', $user, $password );
The parameters may be defined first and then used later.
DBIx::SQLEngine->define_named_connections( 'production' => [ 'dbi:mysql:livedata', \$1, \$2 ], ); $sqldb = DBIx::SQLEngine->new( 'production', $user, $password );
Select to Retrieve Data
The following methods may be used to retrieve data using SQL select statements.
- fetch_select()
-
$sqldb->fetch_select( %sql_clauses ) : $row_hashes $sqldb->fetch_select( %sql_clauses ) : ($row_hashes,$column_hashes)
Retrieve rows from the datasource as an array of hashrefs. If called in a list context, also returns an array of hashrefs containing information about the columns included in the result set.
- visit_select()
-
$sqldb->visit_select( $code_ref, %sql_clauses ) : @results $sqldb->visit_select( %sql_clauses, $code_ref ) : @results
Retrieve rows from the datasource as a series of hashrefs, and call the user provided function for each one. Returns the results returned by each of those function calls.
Queries can use their own SQL queries and placeholder values.
$hashes = $sqldb->fetch_select( sql => [ 'select * from students where status = ?', 'minor' ] );
Data-driven SQL generation converts arguments to queries.
$hashes = $sqldb->fetch_select( table => 'students', where => { 'status' => 'minor' } );
Visit methods allow processing results one row at a time.
my @firstnames = $sqldb->visit_select( table => 'student', order => 'name', sub { my $student = shift; ( $student->{name} =~ /(\w+)\s/ ) ? $1 : $student->{name}; }, );
Limit and offset arguments retrieve a subset of the rows.
$hash_ary = $sqldb->fetch_select( table => 'students', order => 'name', limit => 20, offset => 100, );
Inner joins can be specified by a hash of joining criteria.
$hashes = $sqldb->fetch_select( tables => { 'students.id' => 'grades.student_id' }, order => 'students.name' );
Joins can also be constructed between multiple tables.
$hashes = $sqldb->fetch_select( tables => [ 'students', INNER_JOIN=>['students.id = grades.student_id'], 'grades', INNER_JOIN=>['classes.id = grades.class_id' ], 'classes', ], order => 'students.name' );
Unions combine the results of multiple queries.
$hash_ary = $sqldb->fetch_select( union=>[ { table=>'students', columns=>'first_name, last_name' }, { table=>'staff', columns=>'name_f, name_l' }, ], );
Insert, Update and Delete
You can perform database modifications with these methods.
- do_insert()
-
$sqldb->do_insert( %sql_clauses ) : $row_count
Insert a single row into a table in the datasource. Should return 1, unless there's an exception.
- do_update()
-
$sqldb->do_update( %sql_clauses ) : $row_count
Modify one or more rows in a table in the datasource.
- do_delete()
-
$sqldb->do_delete( %sql_clauses ) : $row_count
Delete one or more rows in a table in the datasource.
Here's a simple insert using a hash of column-value pairs:
$sqldb->do_insert( table => 'students', values => { 'name'=>'Dave', 'age'=>'19', 'status'=>'minor' } );
Here's a basic update statement with a hash of columns-value pairs to change:
$sqldb->do_update( table => 'students', where => 'age > 20', values => { 'status'=>'adult' } );
Here's a basic delete with a table name and criteria.
$sqldb->do_delete( table => 'students', where => { 'name'=>'Dave' } );
Named Query Catalog
These methods manage a collection of named query definitions.
- define_named_queries()
-
$sqldb->define_named_queries( $query_name, $query_info )
Defines one or more named queries using the names and definitions provided.
Here's a defined query used to fetch matching rows.
$sqldb->define_named_query( 'minor_students' => { table => 'students', where => { 'status' => 'minor' } } ); $hashes = $sqldb->fetch_select( named_query => 'minor_students' );
This defined query generates SQL for an update.
$sqldb->define_named_query( 'majority_age' => { action => 'update', table => 'students', where => 'age > 20', values => { 'status'=>'adult' } } ); $rowcount = $sqldb->do_update( named_query => 'majority_age' );
The placeholder in this defined query is replaced at run-time.
$sqldb->define_named_query( 'delete_name' => { action => 'delete', table => 'students', where => { 'name'=> \$1 } } ); $rowcount = $sqldb->do_delete( named_query => [ 'delete_name', 'Dave' ] );
SCHEMA LAYER
The following provides a brief overview of methods provided by the schema classes.
Enumerating TableSets
A Schema::TableSet is simply an array of Schema::Table objects.
-
$tableset = $sqldb->tables();
-
print $tableset->count;
-
foreach my $table ( $tableset->tables ) { print $table->name; }
-
$table = $tableset->table_named( $name );
Querying Table Objects
Table objects pass the various fetch_ and do_ methods through to the SQLEngine Driver along with their table name.
-
$table = $sqldb->table( $table_name );
-
$hash_ary = $table->fetch_select( where => { status=>2 } );
-
$table->do_insert( values => { somefield=>'A Value', status=>3 } );
-
$table->do_update( values => { status=>3 }, where => { status=>2 } );
-
$table->do_delete( where => { somefield=>'A Value' } );
For more information see the documentation for these packages: DBIx::SQLEngine::Schema::Table, DBIx::SQLEngine::Schema::TableSet, DBIx::SQLEngine::Schema::Column, and DBIx::SQLEngine::Schema::ColumnSet.
RECORD LAYER
The following provides a brief overview of methods provided by the record classes.
Setting Up a Record Class
-
$class_name = $sqldb->record_class( $table_name );
-
$sqldb->record_class( $table_name, $class_name );
Selecting Records
-
$record_set = $class_name->fetch_select( criteria => { status=>2 } ); @records = $record_set->records;
-
$record = $class_name->fetch_record( $primary_key );
Changing Records
-
$record = $class_name->new_with_values( somefield=>'A Value' ); $record->insert_record();
-
$record->change( somefield=>'New Value' ); $record->update_record();
-
$record->delete_record();
For more information see the documentation for these packages: DBIx::SQLEngine::Record::Base and DBIx::SQLEngine::Record::Set.
EXAMPLES
The following three examples, based on a writeup by Ron Savage, show a connection being opened, a table created, several rows of data inserted, and then retrieved again. Each uses one of the Driver, Table, or Record interfaces to accomplish the same tasks.
This example uses the basic Driver interface:
#!/usr/bin/perl use strict; use warnings; use DBIx::SQLEngine; my $engine = DBIx::SQLEngine->new( 'DBI:mysql:test', 'route', 'bier' ); my $table_name = 'sqle'; my $columns = [ { name => 'sqle_id', type => 'sequential', }, { name => 'sqle_name', type => 'text', length => 255, }, ]; $engine->drop_table($table_name); $engine->create_table($table_name, $columns); $engine->do_insert(table=>$table_name, values=>{sqle_name=>'One'}); $engine->do_insert(table=>$table_name, values=>{sqle_name=>'Two'}); $engine->do_insert(table=>$table_name, values=>{sqle_name=>'Three'}); my $dataset = $engine->fetch_select(table => $table_name); my $count = 0; for my $data (@$dataset) { $count++; print "Row $count: ", map( {"\t$_ => " . (defined $$data{$_} ? $$data{$_} : 'NULL') } sort keys %$data), "\n"; }
The following example shows an identical series of operations using the Table interface:
#!/usr/bin/perl use strict; use warnings; use DBIx::SQLEngine; my $engine = DBIx::SQLEngine->new( 'DBI:mysql:test', 'route', 'bier' ); my $table = $engine->table( 'sqle' ); my $columns = [ { name => 'sqle_id', type => 'sequential', }, { name => 'sqle_name', type => 'text', length => 255, }, ]; $table->drop_table(); $table->create_table($columns); $table->insert_rows({sqle_name=>'One'}, {sqle_name=>'Two'}, {sqle_name=>'Three'}); my $dataset = $table->fetch_select(); my $count = 0; for my $data (@$dataset) { $count++; print "Row $count: ", map( {"\t$_ => " . (defined $$data{$_} ? $$data{$_} : 'NULL') } sort keys %$data), "\n"; }
This example shows the same operations using the Record interface:
#!/usr/bin/perl use strict; use warnings; use DBIx::SQLEngine; my $engine = DBIx::SQLEngine->new( 'DBI:mysql:test', 'route', 'bier' ); $engine->record_class( 'sqle', 'My::Records' ); my $columns = [ { name => 'sqle_id', type => 'sequential', }, { name => 'sqle_name', type => 'text', length => 255, }, ]; My::Records->drop_table(); My::Records->create_table($columns); My::Records->new_and_save( sqle_name=>'One' ); My::Records->new_and_save( sqle_name=>'Two' ); My::Records->new_and_save( sqle_name=>'Three' ); my $dataset = My::Records->fetch_select(); my $count = 0; for my $data (@$dataset) { $count++; print "Row $count: ", map( {"\t$_ => " . (defined $$data{$_} ? $$data{$_} : 'NULL') } sort keys %$data), "\n"; }
BUGS
Many types of databases are not yet directly supported. While the default functionality should work with any DBI-accessible data source, support for complex queries and advanced features will typically require the addition of a Driver subclass which compensates for local idiom.
Some of the more advanced capabililities have only been added recently, and have not yet been tested in real-world conditions.
See DBIx::SQLEngine::ToDo for a list of bugs and missing features.
SEE ALSO
The driver interface is described in DBIx::SQLEngine::Driver.
See DBIx::SQLEngine::ReadMe for distribution and support information.
See DBI and the various DBD modules for information about the underlying database interface.
See DBIx::AnyDBD for details on the dynamic subclass selection mechanism.
CREDITS AND COPYRIGHT
Author
Developed by Matthew Simon Cavalletto at Evolution Softworks.
You may contact the author directly at evo@cpan.org
or simonm@cavalletto.org
. More free Perl software is available at www.evoscript.org
.
Contributors
Many thanks to the kind people who have contributed code and other feedback:
Eric Schneider, Evolution Online Systems
E. J. Evans, Evolution Online Systems
Matthew Sheahan, Evolution Online Systems
Eduardo Iturrate, Evolution Online Systems
Ron Savage
Christian Glahn, Innsbruck University
Michael Kroll, Innsbruck University
Source Material
Inspiration, tricks, and bits of useful code were mined from these CPAN modules:
Alzabo by Dave Rolsky
DBIx::AnyDBD by Matt Sergeant
DBIx::Compat by G. Richter
DBIx::Datasource by Ivan Kohler
DBIx::Renderer by Marcel Grunauer
Copyright
Copyright 2001, 2002, 2003, 2004 Matthew Cavalletto.
Portions copyright 1998, 1999, 2000, 2001 Evolution Online Systems, Inc.
Portions copyright 2002 ZID, Innsbruck University (Austria).
Portions of the documentation are copyright 2003 Ron Savage.
License
You may use, modify, and distribute this software under the same terms as Perl.