dbix::class vs. dbix::datamodel
TRANSCRIPT
12.04.23 - Page 1
DépartementOffice
DBIx::Classvs.
DBIx::DataModel
FPW 2012, Strasbourg
[email protected]épartement
Office
Agenda
• Introduction• Schema definition• Data retrieval• Joins• Classes and methods• Resultset/Statement objects• Customization• Conclusion
Introduction
ORM layer
Database
DBD driver
DBI
Object-Relational Mapper
Perl program
ORM useful for …
• dynamic SQL generation– navigation between tables– generate complex SQL queries from Perl datastructures– better than phrasebook or string concatenation
• automatic data conversions (inflation / deflation)• transaction encapsulation • data validation• computed fields• caching• expansion of tree data structures coded in the relational
model• …
CPAN ORM Landscape
• Discussed here– DBIx::Class (a.k.a. DBIC)– DBIx::DataModel (a.k.a. DBIDM)
• Many other– Rose::DB, Jifty::DBI, Fey::ORM, ORM,
DBIx::ORM::Declarative, Tangram, Coat::Persistent, ORLite,DBR, DBIx::Sunny, DBIx::Skinny, DBI::Easy, …
DISCLAIMER- I'm not an expert of DBIC- I'll try to be neutral in comparisons, but …- Won't cover all topics
A bit of 2005 historyClass::DBI (1999)
Class::DBI::Sweet (29.05.05)- SQL::Abstract
DBIx::DataModel 0.10 (16.09.05)
SQL::Abstract (2001)
DBIx::Class 0.01 (08.08.05)- SQL::Abstract
DBIx::Class 0.03 (19.09.05)- prefetch joins
Class::DBI::Sweet 0.02 (29.06.05)-prefetch joins
DBIx::DataModel private (feb.05)
Some figures
• DBIC– thousands of users– dozens of contributors– 162 files– 167 packages– 1042 subs/methods– 16759 lines of Perl– 1817 comment lines– 19589 lines of POD– 70 transitive dependencies
• DBIDM– a couple of users– 1 contributor– 31 files– 40 packages– 175 subs/methods– 3098 lines of Perl– 613 comment lines– 8146 lines of POD– 54 transitive dependencies
straight from CPAN::SQLite
Our example : CPAN model
Author
Distribution Module
1
*
1 1..*
* *
contains ►
depends_on ►
12.04.23 - Page 1
DépartementOffice
Schema definition
12.04.23 - Page 1
DépartementOffice
DBIC Schema class
use utf8;package FPW12::DBIC;use strict;use warnings;
use base 'DBIx::Class::Schema';
__PACKAGE__->load_namespaces;
1;
12.04.23 - Page 1
DépartementOffice
DBIC 'Dist' table class
use utf8;package FPW12::DBIC::Result::Dist;use strict; use warnings;use base 'DBIx::Class::Core';
__PACKAGE__->table("dists");__PACKAGE__->add_columns(
dist_id => { data_type => "integer", is_nullable => 0, is_auto_increment => 1 },
dist_vers => { data_type => "varchar", is_nullable => 1, size => 20 }, ... # other columns );__PACKAGE__->set_primary_key("dist_id");__PACKAGE__->belongs_to ('auth', 'FPW12::DBIC::Result::Auth', 'auth_id');__PACKAGE__->has_many ('mods', 'FPW12::DBIC::Result::Mod', 'dist_id');__PACKAGE__->has_many ('depends', 'FPW12::DBIC::Result::Depends', 'dist_id');__PACKAGE__->many_to_many(prereq_mods => 'depends', 'mod');
# idem for classes Auth, Mod, etc.
DBIDM centralized declarations
use DBIx::DataModel;DBIx::DataModel->Schema("FPW12::DBIDM")# class table primary key # ===== ===== ===========->Table(qw/Auth auths auth_id /)->Table(qw/Dist dists dist_id /)->Table(qw/Mod mods mod_id /)->Table(qw/Depends depends dist_id mod_id/)# class role multipl. (optional join keys) # ===== ==== ======= ====================->Association([qw/Auth auth 1 auth_id /], [qw/Dist dists * auth_id /])->Composition([qw/Dist dist 1 /], [qw/Mod mods * /])->Association([qw/Dist dist 1 /], [qw/Depends depends * /])->Association([qw/Mod mod 1 /], [qw/Depends depends * /])->Association([qw/Dist used_in_dist * depends distrib /], [qw/Mod prereq_mods * depends mod /]);
12.04.23 - Page 1
DépartementOffice
Schema def. comparison
• DBIC– one file for each class– regular Perl classes– full column info– 1-way "relationships"
• belongs_to• has_many• may_have• ….
– custom join conditions• any operator• 'foreign' and 'self'
• DBIDM– centralized file– dynamic class creation – no column info (except pkey)– 2-ways "associations" with
• multiplicities• role names• diff association/composition
– custom column names• only equalities
12.04.23 - Page 1
DépartementOffice
Data retrieval
12.04.23 - Page 1
DépartementOffice
DBIC: Search
use FPW12::DBIC;
my $schema = FPW12::DBIC->connect($data_source);
my @dists = $schema->resultset('Dist')->search( {dist_name => {-like => 'DBIx%'}}, {columns => [qw/dist_name dist_vers/]},);
foreach my $dist (@dists) { printf "%s (%s)\n", $dist->dist_name, $dist->dist_vers;}
12.04.23 - Page 1
DépartementOffice
DBIDM: Search
use FPW12::DBIDM;use DBI;
my $datasource = "dbi:SQLite:dbname=../cpandb.sql";my $dbh = DBI->connect($datasource, "", "",
{RaiseError => 1, AutoCommit => 1});FPW12::DBIDM->dbh($dbh);
my $dists = FPW12::DBIDM::Dist->select( -columns => [qw/dist_name dist_vers/], -where => {dist_name => {-like => 'DBIx%'}},);
foreach my $dist (@$dists) { print "$dist->{dist_name} ($dist->{dist_vers})\n";}
12.04.23 - Page 1
DépartementOffice
Simple search comparison
• DBIC– schema is an object– result is a list or a resultset
object– accessor methods for
columns– uses SQL::Abstract
• mostly hidden
• DBIDM– schema is a class (default) or an
object– result is an arrayref (default)– hash entries for columns– uses SQL::Abstract::More
• user can supply a custom $sqla obj
12.04.23 - Page 1
DépartementOffice
SQL::Abstract new()
• special operators
– ex: DBMS-independent fulltext search
# where {field => {-contains => [qw/foo bar/]}
my $sqla = SQL::Abstract->new(special_ops => [ {regex => qr/^contains(:?_all|_any)?$/i, handler => sub { my ($self, $field, $op, $arg) = @_; my $connector = ($op =~ /any$/) ? ' | ' : ' & '; my @vals = ref $arg ? @$arg : ($arg); @vals = map { split /\s+/ } grep {$_} @vals; my $sql = sprintf "CONTAINS($field, '%s') > 0",
join $connector, @vals; return ($sql); # no @bind
}]);
12.04.23 - Page 1
DépartementOffice
DBIC: Find single record
my $auth1 = $schema->resultset('Auth')->find(123);
my $auth2 = $schema->resultset('Auth') ->find({cpanid => 'DAMI'});
12.04.23 - Page 1
DépartementOffice
DBIDM: Find single record
my $auth1 = FPW12::DBIDM::Auth->fetch(123);
my $auth2 = FPW12::DBIDM::Auth->search( -where => {cpanid => 'DAMI'}, -result_as => 'firstrow',);
12.04.23 - Page 1
DépartementOffice
DBIC: Search args
• 1st arg : "where" criteria• 2nd arg : attributes
– distinct– order_by– group_by, having– columns # list of columns to retrieve from main table– +columns # additional columns from joined tables or
from functions – join # see later– prefetch # see later– for– page, rows, offset, etc.– cache
DBIDM: Select args
my $result = $source->select( -columns => \@columns,, -where => \%where, -group_by => \@groupings, -having => \%criteria, -order_by => \@order, -for => 'read only', -post_SQL => sub { … }, -pre_exec => sub { … }, -post_exec => sub { … }, -post_bless => sub { … }, -page_size => …, -page_index => …, -limit => …, -offset => …, -column_types => \%types, -result_as => $result_type,);
DBIDM: Polymorphic result
-result_as =>– 'rows' (default) : arrayref of row objects– 'firstrow' : a single row object (or undef)– 'hashref' : hashref keyed by primary keys– [hashref => @cols] : cascaded hashref– 'flat_arrayref' : flattened values from each row– 'statement' : a statement object (iterator)– 'fast_statement' : statement reusing same memory– 'sth' : DBI statement handle– 'sql' : ($sql, @bind_values)– 'subquery' : \["($sql)", @bind]
don't need method variants : select_hashref(), select_arrayref(), etc.
DBIDM: Fast statement
• like a regular statement– but reuses the same memory location for each row– see DBI::bind_col()
my $statement = $source->select( . . . , -result_as => 'fast_statement');
while (my $row = $statement->next) { . . . # DO THIS : print $row->{col1}, $row->{col2} # BUT DON'T DO THIS : push @results, $row;}
Advanced search comparison
• DBIC– 'find' by any unique
constraint– "inner" vs "outer" columns– result is context-dependent– optional caching
• DBIDM– 'fetch' by primary key only– all columns equal citizens– polymorphic result– no caching– statement-specific inflators– callbacks
Joins
Task : list distributions and their authors
Author
Distribution Module
1
*
1 1..*
* *
contains ►
depends_on ►
DBIC: Join
my @dists = $schema->resultset('Dist')->search( {dist_name => {-like => 'DBIx%'}}, {columns => [qw/dist_name dist_vers/], join => 'auth',
'+columns' => [{'fullname' => 'auth.fullname'}], },);
foreach my $dist (@dists) { printf "%s (%s) by %s\n",
$dist->dist_name, $dist->dist_vers, $dist->get_column('fullname'); # no accessor meth}
DBIC: Join with prefetch
my @dists = $schema->resultset('Dist')->search( {dist_name => {-like => 'DBIx%'}}, {columns => [qw/dist_name dist_vers/], prefetch => 'auth', },);
foreach my $dist (@dists) { printf "%s (%s) by %s\n",
$dist->dist_name, $dist->dist_vers, $dist->auth->fullname; # already in memory}
DBIDM: Join
my $rows = FPW12::DBIDM->join(qw/Dist auth/)->select( -columns => [qw/dist_name dist_vers fullname/], -where => {dist_name => {-like => 'DBIx%'}},);
foreach my $row (@$rows) { print "$row->{dist_name} ($row->{dist_vers}) " . "by $row->{fullname}\n";}
Dist Auth
..::AutoJoin::…
DBIDM::Source::Join
new class created on the fly
Multiple joins
• DBIC
join => [ { abc => { def => 'ghi' } }, { jkl => 'mno' },
'pqr' ]
• DBIDM
->join(qw/Root abc def ghi jkl mno pqr/)
Join from an existing record
• DBIC
my $rs = $auth->dists(undef, {join|prefetch => 'mods'});
• DBIDM
my $rows = $auth->join(qw/dists mods/)->select;
DBIC: left/inner joins
• attribute when declaring relationships# in a Book class (where Author has_many Books)
__PACKAGE__->belongs_to( author => 'My::DBIC::Schema::Author', 'author', { join_type => 'left' } );
• cannot change later (when invoking the relationship)
DBIDM: Left / inner joins
->Association([qw/Author author 1 /], [qw/Distribution distribs 0..* /])
# default : LEFT OUTER JOIN
->Composition([qw/Distribution distrib 1 /], [qw/Module modules 1..* /]);
# default : INNER JOIN
# but defaults can be overriddenMy::DB->join([qw/Author <=> distribs/)-> . . . My::DB->join([qw/Distribution => modules /)-> . . .
Join comparison
• DBIC– rows belong to 1 table class
only– joined columns are either
• "side-products", • prefetched (all of them, no
choice)
– fixed join type
• DBIDM– rows inherit from all joined
tables– flattening of all columns
– default join type, can be overridden
Speed
12.04.23 - Page 1
DépartementOffice
Select speed
list mods(109349 rows)
join Auth dist mods(113895 rows)
DBI 0.43 secs 1.36 secs
DBIC regular 11.09 secs 15.50 secs
DBIC prefetch n.a. 146.29 secs
DBIC hashref inflator
10.06 secs 14.17 secs
DBIDM regular 4.00 secs 5.01 secs
DBIDM fast_statement
2.25 secs 3.28 secs
12.04.23 - Page 1
DépartementOffice
Insert speed
insert (72993 rows)
DBI 5.8 secs
DBIC regular 40.35 secs
DBIC populate 5.6 secs
DBIDM regular
32.81 secs
DBIDM bulk 32.57 secs
12.04.23 - Page 1
DépartementOffice
Classes and methods
DBIC : methods of a row
DB<1> m $auth # 152 methods_auth_id_accessor_cpanid_accessor_email_accessor_fullname_accessor_result_source_instance_accessoradd_to_distsauth_idcpaniddistsdists_rsemailfullnameresult_source_instancevia DBIx::Class::Core -> DBIx::Class::Relationship ->
DBIx::Class::Relationship::Helpers -> DBIx::Class::Relationship::HasMany: has_many
via DBIx::Class::Core -> DBIx::Class::Relationship -> DBIx::Class::Relationship::Helpers -> DBIx::Class::Relationship::HasOne: _get_primary_key
via DBIx::Class::Core -> DBIx::Class::Relationship -> DBIx::Class::Relationship::Helpers -> DBIx::Class::Relationship::HasOne: _has_one
via DBIx::Class::Core -> DBIx::Class::Relationship -> DBIx::Class::Relationship::Helpers -> DBIx::Class::Relationship::HasOne: _validate_has_one_condition
via DBIx::Class::Core -> DBIx::Class::Relationship -> DBIx::Class::Relationship::Helpers -> DBIx::Class::Relationship::HasOne: has_one
...
DB<4> x mro::get_linear_isa(ref $auth)0 ARRAY(0x13826c4) 0 'FPW12::DBIC::Result::Auth' 1 'DBIx::Class::Core' 2 'DBIx::Class::Relationship' 3 'DBIx::Class::Relationship::Helpers' 4 'DBIx::Class::Relationship::HasMany' 5 'DBIx::Class::Relationship::HasOne' 6 'DBIx::Class::Relationship::BelongsTo' 7 'DBIx::Class::Relationship::ManyToMany' 8 'DBIx::Class' 9 'DBIx::Class::Componentised' 10 'Class::C3::Componentised' 11 'DBIx::Class::AccessorGroup' 12 'Class::Accessor::Grouped' 13 'DBIx::Class::Relationship::Accessor' 14 'DBIx::Class::Relationship::CascadeActions' 15 'DBIx::Class::Relationship::ProxyMethods' 16 'DBIx::Class::Relationship::Base' 17 'DBIx::Class::InflateColumn' 18 'DBIx::Class::Row' 19 'DBIx::Class::PK::Auto' 20 'DBIx::Class::PK' 21 'DBIx::Class::ResultSourceProxy::Table' 22 'DBIx::Class::ResultSourceProxy'
DBIDM : methods of a row
DB<1> m $auth # 36 methodsdistsinsert_into_distsmetadmvia DBIx::DataModel::Source::Table:
_get_last_insert_idvia DBIx::DataModel::Source::Table:
_insert_subtreesvia DBIx::DataModel::Source::Table: _rawInsertvia DBIx::DataModel::Source::Table: _singleInsertvia DBIx::DataModel::Source::Table:
_weed_out_subtreesvia DBIx::DataModel::Source::Table: deletevia DBIx::DataModel::Source::Table:
has_invalid_columnsvia DBIx::DataModel::Source::Table: insertvia DBIx::DataModel::Source::Table: updatevia DBIx::DataModel::Source::Table ->
DBIx::DataModel::Source: TO_JSONvia DBIx::DataModel::Source::Table ->
DBIx::DataModel::Source: apply_column_handlervia DBIx::DataModel::Source::Table ->
DBIx::DataModel::Source: auto_expandvia DBIx::DataModel::Source::Table ->
DBIx::DataModel::Source: bless_from_DBvia DBIx::DataModel::Source::Table ->
DBIx::DataModel::Source: expandvia DBIx::DataModel::Source::Table ->
DBIx::DataModel::Source: fetch
DB<4> x mro::get_linear_isa(ref $auth)
0 ARRAY(0x145275c)
0 'FPW12::DBIDM::Auth'
1 'DBIx::DataModel::Source::Table'
2 'DBIx::DataModel::Source'
BelongsTo, HasMany, etc.
Schema Core ResultSet
My::DB My::DB::Result::Table_n
row resultset
DBIC classes
application classes
objects
schema
Class
Row
Componentised AccessorGroup
My::DB::ResultSet::Table_n
Relationship InflateColumn PK::Auto PK RSProxy::Table
BelongsTo, HasMany, etc.
RSProxyRLHelpers RLBase ResultSource
RS::Table
inheritanceinstantiation delegation
Schema
Source
Table Join Statement
My::DB My::DB::Table_n
My::DB::AutoJoin::
row statementrow
DBIDM classes
applicationclasses
objects
schema
DBIDM Meta-Architecture
Schema Table Join
My::DB
My::DB::Table_n
My::DB::Auto_join
Meta::Source
Meta::Table Meta::Join
meta::table
meta::join
meta::schema
Meta::Schema Meta::PathMeta::Association Meta::Type
meta::assoc meta::path
meta::type
DBIC introspection methods
• Schema– sources(), source($source_name)
• ResultSource– primary_key– columns(), column_info($column_name)– relationships(), relationship_info($relname)
Architecture comparison
• DBIC– very complex class structure– no distinction front/meta
layser– methods for
• CRUD• navigation to related objs• introspection• setting up columns• setting up relationships• …
• DBIDM– classes quite close to DBI
concepts– front classes and Meta
classes– methods for
• CRUD• navigation to related objs• access to meta
12.04.23 - Page 1
DépartementOffice
Resultset/Statement objects
12.04.23 - Page 1
DépartementOffice
Example task
• list names of authors – of distribs starting with 'DBIx' – and version number > 2
12.04.23 - Page 1
DépartementOffice
DBIC ResultSet chaining
my $dists = $schema->resultset('Dist')->search( {dist_name => {-like => 'DBIx%'}},);my $big_vers = $dists->search({dist_vers => { ">" => 2}});my @auths = $big_vers->search_related('auth', undef,
{distinct => 1, order_by => 'fullname'});
say $_->fullname foreach @auths;
# Magical join ! Magical "group by" !SELECT auth.auth_id, auth.email, auth.fullname, auth.cpanidFROM dists me JOIN auths auth ON auth.auth_id = me.auth_id WHERE ( ( dist_vers > ? AND dist_name LIKE ? ) ) GROUP BY auth.auth_id, auth.email, auth.fullname, auth.cpanid ORDER BY fullname
DBIDM Statement lifecycle
new
sqlized
prepared
executed
schema + source
data row(s)
new()
sqlize()
prepare()
execute()
bind()refine()
bind()
bind()
bind()execute()
next() / all()
blessedcolumn types applied
-post_bless
-pre_exec
-post_exec
-post_SQL
DBIDM Statement::refine()
my $stmt = FPW12::DBIDM->join(qw/Dist auth/)->select( -where => {dist_name => {-like => 'DBIx%'}}, -result_as => 'statement',);
$stmt->refine( -columns => [-DISTINCT => 'fullname'], -where => {dist_vers => { ">" => 0}}, -order_by => 'fullname',);
say $_->{fullname} while $_ = $stmt->next;
DBIDM subquery
my $stmt = FPW12::DBIDM::Dist->select( -where => {dist_name => {-like => 'DBIx%'}}, -result_as => 'statement',);
my $subquery = $stmt->select( -columns => 'auth_id', -where => {dist_vers => { ">" => 2}}, -result_as => 'subquery',);
my $auths = FPW12::DBIDM::Auth->select( -columns => 'fullname', -where => {auth_id => {-in => $subquery}},);
say $_->{fullname} foreach @$rows;
Statement comparison
• DBIC– powerful refinement
constructs• more "where" criteria• navigation to related source• column restriction• aggregation operators (e.g.
"count")
• DBIDM– limited refinement constructs– explicit control of status
12.04.23 - Page 1
DépartementOffice
Other features
12.04.23 - Page 1
DépartementOffice
Transactions
• DBIC
$schema->txn_do($sub);
– can be nested– can have intermediate
savepoints
• DBIDM
$sch->do_transaction($sub);
– can be nested– no intermediate savepoints
DBIC inflation/deflation
__PACKAGE__->inflate_column('insert_time', {inflate => sub { DateTime::Format::Pg->parse_datetime(shift); }, deflate => sub { DateTime::Format::Pg->format_datetime(shift) }, });
DBIDM Types (inflate/deflate)
# declare a TypeMy::DB->Type(Multivalue => from_DB => sub {$_[0] = [split /;/, $_[0]] }, to_DB => sub {$_[0] = join ";", @$_[0] },);
# apply it to some columns in a tableMy::DB::Author->metadm->define_column_type( Multivalue => qw/hobbies languages/,);
Extending/customizing DBIC
• Subclassing– result classes– resultset classes– storage– sqlmaker
Extending / customizing DBIDM
• Schema hooks for– SQL dialects (join syntax, alias syntax, limit / offset, etc.)– last_insert_id
• Ad hoc subclasses for– SQL::Abstract– Table– Join– Statements
• Statement callbacks• Extending table classes
– additional methods– redefining _singleInsert method
Not covered here
• inserts, updates, deletes• Schema generator• Schema versioning• inflation/deflation
12.04.23 - Page 1
DépartementOffice
THANK YOU FOR YOUR ATTENTION
12.04.23 - Page 1
DépartementOffice
Bonus slides
Why hashref instead of OO accessors ?
• Perl builtin rich API for hashes (keys, values, slices, string interpolation)
• good for import / export in YAML/XML/JSON• easier to follow steps in Perl debugger• faster than OO accessor methods• visually clear distinction between lvalue / rvalue
– my $val = $hashref->{column};– $hashref->{column} = $val;
• visually clear distinction between – $row->{column} / $row->remote_table()
SQL::Abstract::More : extensions
• -columns => [qw/col1|alias1 max(col2)|alias2/]– SELECT col1 AS alias1, max(col2) AS alias2
• -columns => [-DISTINCT => qw/col1 col2 col3/]– SELECT DISTINCT col1, col2, col3
• -order_by => [qw/col1 +col2 –col3/]– SELECT … ORDER BY col1, col2 ASC, col3 DESC
• -for => "update" || "read only"– SELECT … FOR UPDATE