DBIx::Class

Contact Info

Authors

Arthur Axel "fREW" Schmidt

What's up guys?

Purpose

The purpose of this talk is to show you as many features of DBIx::Class in 40 minutes so that when you need to do something with it later you will know what's possible

DBIx::Class?

Meta

These are reasons that are not technical or inherent to the code of DBIC, but are totally awesome things about it.

Large Community

Currently there are 118 people listed as contributors to DBIC. That ranges from documentation help, to test help, to added features, to entire database support.

Active Community

Upcoming release (0.08197?) has 8 new features, and 26 bug fixes. Of course that ebbs and flows.

Responsive Community

General ORM

These are things that are in most (but not all!) other ORMs, but are still reasons to use DBIC over raw SQL.

Cross DB

The vast majority of code should run on all databases without needing tweaking

Basic CRUD

SQL: Create

my $sth = $dbh->prepare('
   INSERT INTO books
   (title, author_id)
   values (?,?)
');

$sth->execute(
  'A book title', $author_id
);

DBIC: Create

my $book = $book_rs->create({
   title     => 'A book title',
   author_id => $author_id,
});

SQL: Read

my $sth = $dbh->prepare('
   SELECT title,
   authors.name as author_name
   FROM books, authors
   WHERE books.author = authors.id
');

while( my $book = $sth->fetchrow_hashref() ) {
  print 'Author of '
     . $book->{title}
     . ' is '
     . $book->{author_name}
     . "\n";
}

DBIC: Read

my $book = $book_rs->find($book_id);

my $book = $book_rs->search({
   title => 'A book title',
}, { rows => 1 })->next;

my @books = $book_rs->search({
   author => $author_id,
})->all;

while( my $book = $books_rs->next ) {
 print 'Author of '
    . $book->title
    . ' is '
    . $book->author->name
    . "\n";
}

SQL: Update

my $update = $dbh->prepare('
   UPDATE books
   SET title = ?
   WHERE id = ?
');

$update->execute(
  'New title',$book_id
);

DBIC: Update

$book->update({
  title => 'New title',
});

SQL: Delete

my $delete = $dbh->prepare('
   DELETE FROM books
   WHERE id = ?
');

$delete->execute($book_id);

DBIC: Delete

$book->delete;

SQL: Search

my $sth = $dbh->prepare('
   SELECT title,
   authors.name as author_name
   FROM books
   WHERE books.name LIKE "%monte cristo%" AND
   books.topic = "jailbreak"
');

DBIC: Search

my $book = $book_rs->search({
   'me.name'  => { -like => '%monte cristo%' },
   'me.topic' => 'jailbreak',
})->next;

OO Overridability

Convenience Methods

Non-column methods

Need a method to get a user's gravatar URL? Add a gravatar_url method to their Result class

         use Gravatar::URL ();
         sub gravatar_url {
            Gravatar::URL::gravatar_url({ email => $_[0]->email })
         }
      

RELATIONSHIPS

EXTENDED RELATIONSHIPS

 __PACKAGE__->belongs_to(
  output_device => 'My::Schema::Result::OutputDevice',
  sub {
    my $args = shift;

    my $other = $args->{foreign_alias};
    my $me   = $args->{self_alias};
    my $self = $args->{self_rowobj};
    return ({
      "$other.user"   => { -ident => "$me.user" },
      "$other.shared" => 1,
    },
    $self && {
       "$other.user"     => $self->user,
       "$other.shared" => 1,
    });
  },
);

->deploy

Perl -> DB

my $schema = Foo::Schema->connect(
   $dsn, $user, $pass
);
$schema->deploy

Schema::Loader

DB -> Perl

package Foo::Schema;
use base 'DBIx::Class::Schema::Loader';
__PACKAGE__->loader_options({
   naming => 'v7',
   debug  => $ENV{DBIC_TRACE},
});
1;

# elsewhere...

my $schema = Foo::Schema->connect(
   $dsn, $user, $pass
);

Populate

Made for inserting lots of rows very quicky into database

$schema->populate([ Users =>
   [qw( username password )],
   [qw( frew     >=4char$ )],
   [qw(      ...          )],
   [qw(      ...          )],
);

Multicreate

Create an object and all of it's related objects all at once

$schema->resultset('Author')->create({
   name => 'Stephen King',
   books => [{ title => 'The Dark Tower' }],
   address => {
      street => '123 Turtle Back Lane',
      state  => { abbreviation => 'ME' },
      city   => { name => 'Lowell'     },
   },
});

Extensible

DBIx::Class helped pioneer fast MI in Perl 5 with Class::C3, so it is made to allow extensions to nearly every part of it. (more slides about components later)

Result vs ResultSet

ResultSet methods

package MyApp::Schema::ResultSet::Book;
use base 'DBIx::Class::ResultSet';
sub good {
   my $self = shift;
   $self->search({
      $self->current_source_alias .
         '.rating' => { '>=' => 4 },
   })
}
sub cheap {
   my $self = shift;
   $self->search({
      $self->current_source_alias .
         '.price' => { '<=' => 5}
   })
}
      

ResultSet method notes

ResultSet method in Action

$schema->resultset('Book')->good

ResultSet Chaining

$schema->resultset('Book')
   ->good
   ->cheap
   ->recent

search_related

my $score = $schema->resultset('User')
   ->search({'me.userid' => 'frew'})
   ->related_resultset('access')
   ->related_resultset('mgmt')
   ->related_resultset('orders')
   ->telephone
   ->search_related( shops => {
      'shops.datecompleted' => {
         -between => ['2009-10-01','2009-10-08']
      }
   })->completed
   ->related_resultset('rpt_score')
   ->get_column('raw_scores')
   ->first;

bonus rel methods

my $book = $author->create_related(
   books => {
      title => 'Another Discworld book',
   }
);

my $book2 = $pratchett->add_to_books({
   title => 'MOAR Discworld book',
});

Excellent Transaction Support

$schema->txn_do(sub {
   ...
});

my $guard = $schema->txn_scope_guard;
# ...
$guard->commit;

$schema->txn_begin; # <-- low level
# ...
$schema->txn_commit;

InflateColumn

package Foo::Schema::Result::Book;
use base 'DBIx::Class::Core';
use DateTime::Format::MySQL;
# Result code here
__PACKAGE__->load_components('InflateColumn');
__PACKAGE__->inflate_column(
   date_published => {
      inflate => sub {
         DateTime::Format::MySQL->parse_date(
            shift
         )
      },
      deflate => sub { shift->ymd },
   },
);
# Automatic see: DBIC::InflateColumn::DateTime

InflateColumn: deflation

$book->date_published(DateTime->now);
$book->update;

InflateColumn: inflation

say $book->date_published->month_abbr;
Nov

FilterColumn

package Foo::Schema::Result::Book;
use base 'DBIx::Class::Core';
# Result code here
__PACKAGE__->load_components('FilterColumn');

__PACKAGE__->filter_column(
   length => {
      to_storage   => 'to_metric',
      from_storage => 'to_imperial',
   },
);

sub to_metric   { $_[1] * .305 }
sub to_imperial { $_[1] * 3.28 }

ResultSetColumn

my $rsc = $schema->resultset('Book')
   ->get_column('price');
$rsc->first;
$rsc->all;
$rsc->min;
$rsc->max;
$rsc->sum;

Aggregates

my @res = $rs->search({}, {
   columns   => [
      'price',
      'genre',
      { max_price => { max => price } },
      { avg_price => { avg => price } },
   ],
   group_by => [qw(price genre)],
});
for (@res) {
   say $_->price . ' ' . $_->genre;
   say $_->get_column('max_price');
   say $_->get_column('min_price');
}

Aggregates Notes

HRI

$rs->search({}, {
  result_class =>
    'DBIx::Class::ResultClass::HashRefInflator',
});

Subquery Support

my $inside_query = $schema->resultset('Artist')
   ->search({
    name => [ 'Billy Joel', 'Iron Maiden' ],
})->get_column('id')->as_query;

my $rs = $schema->resultset('CD')->search({
    artist_id => { -in => $inside_query },
});

Bare SQL w/ Placeholders

$rs->update({
   # !!! SQL INJECTION VECTOR
   price => \"price + $inc",
});

$rs->update({
   price => \['price + ?', [{} => $inc]],
});

Advanced

Random example - SQL metaprogramming

    $shadow_rs->search({ shadow_val_title => 'stab' })->insert({

      shadow_stage => 2,

      shadow_timestamp => 12345678,

      shadowed_lifecycle => \"(
        SELECT COALESCE( MAX( shadowed_lifecycle ), 0 ) + 1 FROM $shadow_table
      )",

      rel_shadow_artists_lifecycle =>
        $shadowed_cd->search_related('artist')
                     ->search_related('shadows', {}, { rows => 1 })
                      ->get_column('shadowed_lifecycle')
                       ->as_query
      ,
    });
INSERT
  INTO shadow_cds (
    shadow_stage,
    shadow_timestamp,
    shadow_val_title,
    shadow_lifecycle,
    rel_shadw_artists_lifecycle
  ) VALUE (
    '2',
    '12345678',
    'stab',
    '2',
    (
      SELECT COALESCE( MAX( shadowed_lifecycle ), 0 ) + 1
        FROM shadow_cds
    ),
    (
      SELECT shadows.shadowed_lifecycle
        FROM artists me
        JOIN shadow_artists shadows
          ON shadows.shadowed_curpk_name = me.name
      WHERE me.name = 'gaga'
      LIMIT '1'
    ),
  )
 

Loosely coupled abstractions

Loosely coupled abstractions

  my $schema_instance               # knows about its registered sources (load_namespaces,
    = Schema::Class->connect(...);  # load_classes, compose_namespace, etc), and about its
                                    # storage instance (the storage IO abstraction)

  my $result_source_instance
    = $schema_instance->source('Artist') # knows how to refer to the actual data source
                                         # when talking to storage, knows the source
                                         # metadata, knows the default result inflation class

  my $resultset_instance            # query plan, holds a reference to the source and infers
    = $result_sources_instance;     # from it the storage-source name, and the default selection
( or $schema->resultset('Artist')   # and/or restrictions from source instance metadata
                                    # ALL OVERRIDABLE

  my $row_object
    = $resultset_instance->next     # actual query execution, raw result is processed and returned
                                    # according to the $resultset_instance settings
                                    # CAN BE ANYTHING

Query definition/manipulation framework (DBIx::Class::ResultSet)

Overridable/augmentable default selection

Overridable/augmentable default selection: AS IS NOT AS

ResultSet operations (CRUD)

Storage I/O abstraction layer (DBIx::Class::Storage(::DBI ))

DBIx::Class::Core / DBIx::Class::Row

Write your own result class

The power of Extended Relationships

Just a coderef, so you can...

DBIx::Class Components

NOT ALL ME, LOTS OF AUTHORS

::Candy

Use this to avoid carpel tunnel (no more __PACKAGE__)

package Lynx::SMS::Schema::Result::Price;

use Lynx::SMS::Schema::Candy; # special subclass

primary_column id => {
  data_type => 'int',
  is_auto_increment => 1,
};

column part_code => {
  data_type => 'varchar',
  size      => 15,
};

column description => {
  data_type       => 'text',
  is_serializable => 1,
};

column type_id          => { data_type => 'int' };
column credits_per_year => { data_type => 'int' };
column list_price       => { data_type => 'money' };
column gsa_price        => { data_type => 'money' };

1;
      

::TimeStamp

USE THIS for any kind of timestamp (set on create or set on update)

Caveat: any bugs in DateTime are also in this

::EncodedColumn

USE THIS for password columns

use DBIx::Class::Candy -components => ['EncodedColumn'];
column password => {
   is_serializable => 0,
   data_type     => 'CHAR',
   size          => 59,
   encode_column => 1,
   encode_class  => 'Crypt::Eksblowfish::Bcrypt',
   encode_args   => { key_nul => 0, cost => 8 },
   encode_check_method => 'check_password',
};

Later..

die "liar!" unless $user->check_password($input)

Detour: Context--

my @foo = $rs->search(...)

vs

my $foo = $rs->search(...)

or more importantly...

$foo->bar($rs->search(...))

::Helper::ResultSet::IgnoreWantarray

USE THIS for less surprising resultsets (must call ->all)

      print_rs($rs->search(...)); # <3
      print_row($_) for $rs->search(...)->all; # <3
      

::Helper::Row::OnColumnChange

USE THIS to do something when a value changes (per column)

use DBIx::Class::Candy -components => [
   'Helper::Row::OnColumnChange'
];

after_column_change kind_of_id => {
   txn_wrap => 1,
   method   => '_fix_for_updates',
};

sub _fix_for_updates {
   my ( $self, $old, $new ) = @_;

   $self->_set_materialized_path;

   $_->_fix_for_updates for $self->direct_kinds->all
}
      

::Helper::ResultSet::SetOperations

USE THIS to use UNION and other set ops with DBIC

(excuse this insane example)
sub succeeded_computers {
   my $self = shift;

   my $c_links = $self->test_computer_links;
   my $d_links = $self->test_device_links;

   my $failed_ids =
      $c_links->failed->related_resultset('computer')->union(
         $c_links->untested->related_resultset('computer'),
      )->union_with_devices(
         $d_links->failed->related_resultset('device')->union(
            $d_links->untested->related_resultset('device'),
         )
      )->get_column('id')->as_query;

   $self->result_source->schema->resultset('Computer')->search({
      id => {
         -not_in => $failed_ids,
         -in     => $c_links->related_resultset('computer')->union_with_devices(
                       $d_links->related_resultset('device')
                    )->get_column('id')->as_query
      }
   });
}
      

::Helper::Row::RelationshipDWIM

USE THIS to avoid carpel tunnel (has_many cars => 'Cars', ...)


      use DBIx::Class::Candy -components => [
         'Helper::Row::RelationshipDWIM'
      ];
      belongs_to type => '::Type', 'type_id';

      

::Helper::Row::StorageValues

USE THIS to keep track of what's in the DB before you call update and after you use an accessor

      use DBIx::Class::Candy -components => [ 'Helper::Row::StorageValues' ];

      # use OnColumnChange for this, just an example (has bug)
      sub update {
         my $self = shift;
         log_trace {
            "name changing from " . $self->storage_value('name') .
               " to " . $self->name
         };

         $self->next::method(@_);
      }
      

::Helper::Row::SubClass

USE THIS to subclass parts of your schema (deprecated by new versions of Class::C3::Componentised)

automatically makes sure to set the same table and tries it's best to copy rels

::Helper::Row::ToJSON

USE THIS to generate a sensible TO_JSON method

::Helper::ResultSet::CorrelateRelationship

USE THIS for getting aggregates (and other things)

see later slide for in depth example

::Helper::ResultSet::SearchOr

USE THIS to reuse even more resultset methods

::Helper::Schema::LintContents

USE THIS to check if the contents of your database are in good shape

::Helper::ResultSet::ResultClassDWIM

USE THIS to avoid carpel tunnel ( result_class => '::HRI' )

::Helper::ResultSet::RemoveColumns

Use this to remove columns from selection in a succicnt fashion

         $rs->search(undef, {
            remove_columns => ['password'],
         })
      
vs
         $rs->search(undef, {
            columns => [qw(id username email fullname ... )],
         })
      

Not just less to type, also more robust

::Helper::ResultSet::Random

Use this to grab random rows from a resultset

         my $rand = $rs->rand(1)->next;
      

::Helper::Row::JoinTable

Use this to reduce boilerplate involved in creating join tables

 package MyApp::Schema::Result::Foo_Bar;

 use DBIx::Class::Candy -components => ['Helper::Row::JoinTable'];

 join_table {
    left_class   => 'Foo',
    left_method  => 'foo',
    right_class  => 'Bar',
    right_method => 'bar',
 };
      

::DeploymentHandler

Useful ResultSet Methods

  1. sub with_all_computers_count {
       my $self = shift;
    
       $self->search(undef, {
          '+columns' => {
             all_computers_count =>
                $self->correlate('computers')->count_rs->as_query
          },
       })
    }
    
  2. ::Helper::Row::ProxyResultSetMethod
    package MyApp::Schema::Result::Test;
    
    use DBIx::Class::Candy -components => [
       'Helper::Row::ProxyResultSetMethod',
    ];
    
    proxy_resultset_method => 'all_computers_count';
             

Questions?

END

/

#