DBIx::Class introduction - 2010

Preview:

DESCRIPTION

If your not using an ORM (object relational mapper) and are still writing SQL by hand, here's what you need to know. An introduction into DBIx::Class and some of the concepts and goodies you should be aware off.

Citation preview

DBIx::Class (aka DBIC)for (advanced) beginners

Leo Lapworth @ YAPC::EU 2010

http://leo.cuckoo.org/projects/

assumptions

You know a little about Perl and using objects

You know a little bit about databases and using foreign keys

DBIx::Class?

• ORM (object relational mapper)

• SQL <-> OO (using objects instead of SQL)

• Simple, powerful, complex, fab and confusing

• There are many ORMs, DBIx::Class just happens to be the best in Perl (personal opinion)

why this talk?

• Help avoid mistakes I made!

• Help learn DBIx::Class faster

• Make your coding easier

table setup

example...

Books

Authors

authors table

CREATE TABLE authors(

id int(8) primary key auto_increment,

name varchar(255)

) engine = InnoDB DEFAULT CHARSET=utf8;

tips

Name tables as simple plurals (add an S) - makes relationships easier to understand

(issue: Matt Trout "Tables should not be plural as gives you plurals for Result:: package names which represent a single row" - talk may be rewritten in future to reflect this as this is better once you understand the relationship setup - either way, consistency is important)

Use a character set (UTF8) from the start (for international characters)

authors table

CREATE TABLE authors(

id int(8) primary key auto_increment,

name varchar(255)

) engine = InnoDB DEFAULT CHARSET=utf8;

books tableCREATE TABLE books(

id int(8) primary key auto_increment,

title varchar(255),

author int(8),

foreign key (author)

references authors(id)

) engine = InnoDB DEFAULT CHARSET=utf8;

tips

Name link fields as singular

Check foreign key is the same field type and size in both tables

books tableCREATE TABLE books(

id int(8) primary key auto_increment,

title varchar(255),

author int(8),

foreign key (author)

references authors(id)) engine = InnoDB DEFAULT CHARSET=utf8;

CRUD comparedC - CreateR - RetrieveU - UpdateD - Delete

Manual (SQL)

manual: createmy $sth = $dbh->prepare('

INSERT INTO books

(title, author)

values (?,?)

');

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

manual: createmy $sth = $dbh->prepare('

INSERT INTO books

(title, author)

values (?,?)

');

$sth->execute(

'A book title',$author_id);

manual: retrievemy $sth = $dbh->prepare('

SELECT title,

authors.name as author_name

FROM books, authors

WHERE books.author = authors.id

');

manual: retrievewhile( my $book = $sth->fetchrow_hashref() ) {

print 'Author of '

. $book->{title}

. ' is '

. $book->{author_name}

. "\n";

}

manual: updatemy $update = $dbh->prepare('

UPDATE books

SET title = ?

WHERE id = ?

');

$update->execute(

'New title',$book_id);

manual: deletemy $delete = $dbh->prepare('

DELETE FROM books

WHERE id = ?

');

$delete->execute($book_id);

DBIx::Class

DBIC: createmy $book = $book_model->create({

title => 'A book title',

author => $author_id,

});

Look ma, no SQL!

Tip: do not pass in primary_key field, even if its empty/undef as the object returned will have an empty id, even if your field is auto increment.

DBIC: createmy $book = $book_model->create({

title => 'A book title',

author => $author_id,});

DBIC: createmy $pratchett = $author_model->create({

name => 'Terry Pratchett',

});

DBIC: createmy $book = $pratchett->create_related(

'books', {

title => 'Another Discworld book',

});

or

my $book = $pratchett->add_to_books({

title => 'Another Discworld book',

});

DBIC: createmy $book = $pratchett->create_related(

'books', {

title => 'Another Discworld book',

});

or

my $book = $pratchett->add_to_books({

title => 'Another Discworld book',

});

DBIC: retrieve

DBIx::Class - Lots of ways to do the same thing...

"There is more than one way to do it (TIMTOWTDI, usually pronounced "Tim Toady") is a Perl motto"

DBIC: retrievemy $book = $book_model->find($book_id);

my $book = $book_model->search({

title => 'A book title',

})->single();

my @books = $book_model->search({

author => $author_id,

})->all();

DBIC: retrievewhile( my $book = $books_rs->next() ) {

print 'Author of '

. $book->title()

. ' is '

. $book->author()->name()

. "\n";

}

DBIC: retrievemy $books_rs = $book_model->search({

author => $author_id,

});

Search takes SQL::Abstract formatted queries> perldoc SQL::Abstract

DBIC: update$book->update({

title => 'New title',

});

DBIC: delete$book->delete();

Creating schemas

too much typing!

too much maintenance!

Schema::Loader

Database introspection -> Code

Use namespaces

Use NamespacesSplits logic cleanly

Bookstore::Schema::Result::X

= an individual row

Bookstore::Schema:: ResultSet::X

= searches / results

You can edit this line

Connection details

using your Schema

DEBUGGING

DBIC_TRACE=1 ./your_script.pl

SQL - debugging

INSERT INTO authors (name) VALUES (?): 'Douglas Adams'

INSERT INTO books (author, title) VALUES (?, ?): '1', '42'

overloading

Bookstore::Schema::Result::Books

Bookstore::Schema::ResultSet::Books

Bookstore::Schema::Result::Authors

Bookstore::Schema::ResultSet::Authors

Result::package Bookstore::Schema::Result::Books;use base 'DBIx::Class';

#...

# Created by DBIx::Class::Schema::Loader v0.04005 @ 2010-08-01 09:19:14# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:ta+cEh31lDfqcue3OmUCfQ

sub isbn {my $self = shift;

# search amazon or somethingmy $api = Amazon::API->book({ title => $self->title() });

return $api->isbn();}

1;

Result::package Bookstore::Schema::Result::Books;use base 'DBIx::Class';

#...

# Created by DBIx::Class::Schema::Loader v0.04005 @ 2010-08-01 09:19:14# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:ta+cEh31lDfqcue3OmUCfQ

sub isbn {my $self = shift;

# search amazon or somethingmy $api = Amazon::API->book({ title => $self->title() });

return $api->isbn();}

1;

Result::print $book->isbn();

Result:: (inflating)package Bookstore::Schema::Result::Books;use base 'DBIx::Class';

#...

use DateTime::Format::MySQL;

__PACKAGE__->inflate_column( 'date_published', { inflate => sub { DateTime::Format::MySQL->parse_date(shift); }, deflate => sub { shift->ymd(); }, });# Automatic see: DBIx::Class::InflateColumn::DateTime

Result:: (inflating)package Bookstore::Schema::Result::Books;use base 'DBIx::Class';

#...

use DateTime::Format::MySQL;

__PACKAGE__->inflate_column( 'date_published', { inflate => sub {

DateTime::Format::MySQL->parse_date(shift); }, deflate => sub { shift->ymd(); }, });# Automatic see: DBIx::Class::InflateColumn::DateTime

Result:: (deflating)$book->date_published(DateTime->now);

$book->update();

2008-12-31

Result:: (inflating)

my $date_published = $book->date_published()print $date_published->month_abbr();

Nov

ResultSets::package Bookstore::Schema::ResultSet::Books;use base 'DBIx::Class::ResultSet';

#...

sub the_ultimate_books { my $self = shift;

return $self->search( { title => { 'like', '%42%' } });}

sub by_author { my ( $self, $author ) = @_;

return $self->search( { author => $author->id(), } );}

1;

ResultSets::package Bookstore::Schema::ResultSet::Books;use base 'DBIx::Class::ResultSet';#...sub the_ultimate_books { my $self = shift;

return $self->search( { title => { 'like', '%42%' } });}

sub by_author { my ( $self, $author ) = @_;

return $self->search( { author => $author->id(), } );}

ResultSets::package Bookstore::Schema::ResultSet::Books;use base 'DBIx::Class::ResultSet';#...sub the_ultimate_books { my $self = shift;

return $self->search( { title => { 'like', '%42%' } });}

sub by_author { my ( $self, $author ) = @_;

return $self->search( { author => $author->id(),

} );}

1;

ResultSets::use Bookstore::Schema;

my $book_model = Bookstore::Schema->resultset('Books');

my $book_rs = $book_model->the_ultimate_books();

my @books = $book_rs->all();

ResultSets::chaininguse Bookstore::Schema;

my $book_model = Bookstore::Schema->resultset('Books');my $author_model = Bookstore::Schema->resultset('Authors');

my $author = $author_model->search({ name => 'Douglas Adams',})->single();

my $book_rs = $book_model->the_ultimate_books() ->by_author($author);

my @books = $book_rs->all();

ResultSets::chainingmy $book_rs = $book_model

->the_ultimate_books() ->by_author($author);

or

my $book_rs = $book_model ->the_ultimate_books();$book_rs = $book_rs->by_author($author);

# Debug (SQL):

# SELECT me.id, me.title, me.date_published, me.author # FROM books me # WHERE ( ( ( author = ? ) AND ( title LIKE ? ) ) ): '1', '%42%'

ResultSets::chainingmy $rs = $book_model

->category('childrens') ->by_author($author) ->published_after('1812') ->first_page_contains('once upon') ->rating_greater_than(4);

my @books = $rs->all();

overloading before new record

overloading before new record

package Bookstore::Schema::Result::Authors;use base 'DBIx::Class';

sub new { my ( $class, $attrs ) = @_;

# Mess with $attrs

my $new = $class->next::method($attrs); return $new;}

1;

relationships

multiple authors

a few relationships

Authors BooksAuthors_and_Books

has_many has_many

belongs_to belongs_to

many_to_many

a few relationships

!

new join tableCREATE TABLE author_and_books( id int(8) primary key auto_increment, book ! int(8), author int(8),

foreign key (book) references books(id), foreign key (author) references authors(id)

) engine = InnoDB DEFAULT CHARSET=utf8;

ALTER TABLE `books` DROP COLUMN `author`;

CREATE TABLE author_and_books( id int(8) primary key auto_increment, book ! int(8), author int(8),

foreign key (book) references books(id), foreign key (author) references authors(id)

) engine = InnoDB DEFAULT CHARSET=utf8;

new join table

has_many

Books Authors_and_Books

has_many

belongs_to

has_manypackage Bookstore::Schema::Result::Books;

__PACKAGE__->has_many(

"author_and_books", "Bookstore::Schema::Result::AuthorAndBooks",

{ "foreign.book" => "self.id" },

);

# This is auto generated by Schema::Loader

has_manypackage Bookstore::Schema::Result::Books;

__PACKAGE__->has_many(

"author_and_books", # Name of accessor "Bookstore::Schema::Result::AuthorAndBooks", # Related class { "foreign.book" => "self.id" }, # Relationship (magic often works if not # specified, but avoid!));

belongs_to

Books Authors_and_Books

has_many

belongs_to

belongs_topackage Bookstore::Schema::Result::AuthorAndBooks;

__PACKAGE__->belongs_to( "book", "Bookstore::Schema::Result::Books", { id => "book" });

# This is auto generated by Schema::Loader

belongs_topackage Bookstore::Schema::Result::AuthorAndBooks;

__PACKAGE__->belongs_to( "book", # Accessor name "Bookstore::Schema::Result::Books",

# Related class { id => "book" } # Relationship);

same for Authors

Authors Authors_and_Books

has_many

belongs_to

with no coding...

Authors BooksAuthors_and_Books

has_many has_many

belongs_to belongs_to

many_to_many

Authors BooksAuthors_and_Books

has_many has_many

belongs_to belongs_to

many_to_many

many_to_manypackage Bookstore::Schema::Result::Books;use base 'DBIx::Class';

__PACKAGE__->many_to_many( "authors"

=> "author_and_books",

'author');

1;

# This is NOT auto generated by Schema::Loader

many_to_manypackage Bookstore::Schema::Result::Books;use base 'DBIx::Class';

__PACKAGE__->many_to_many( "authors" # Accessor Name => "author_and_books", # has_many accessor_name 'author' # foreign relationship name);

1;

many_to_manypackage Bookstore::Schema::Result::Authors;use base 'DBIx::Class';

__PACKAGE__->many_to_many( "books" # Accessor Name => "author_and_books", # has_many accessor_name 'book' # foreign relationship name);

1;

# This is NOT auto generated by Schema::Loader

using many_to_many#!/usr/bin/perl

use Bookstore::Schema;

my $author_model = Bookstore::Schema->resultset('Authors');

my $author = $author_model->search({name => 'Douglas Adams',

})->single();

$author->add_to_books({title => 'A new book',

});

using many_to_manymy $author = $author_model->search({name => 'Douglas Adams',

})->single();

$author->add_to_books({title => 'A new book',

});

# SELECT me.id, me.name FROM authors me # WHERE ( name = ? ): 'Douglas Adams';

# INSERT INTO books (title) VALUES (?): 'A new book';

# INSERT INTO author_and_books (author, book) # VALUES (?, ?): '5', '2';

using many_to_many$author->add_to_books($book);

$book->add_to_authors($author_1);

$book->add_to_authors($author_2);

in 16 lines of code

Authors BooksAuthors_and_Books

has_many has_many

belongs_to belongs_to

many_to_many

errors

Read them closely!

error messagesDBIx::Class::Schema::Loader::connection(): Failed to load external class definition for 'Bookstore::Schema::Result::Authors': Can't locate object method "many_to_many" via package "Bookstore::Schema::Result::Author" at lib/Bookstore/Schema/Result/Authors.pm line 9.Compilation failed in require at /Library/Perl/5.8.8/DBIx/Class/Schema/Loader/Base.pm line 292.

error messagesDBIx::Class::Schema::Loader::connection(): Failed to load external class definition for 'Bookstore::Schema::Result::Authors': Can't locate object method "many_to_many" via package "Bookstore::Schema::Result::Author" at lib/Bookstore/Schema/Result/Authors.pm line 9.Compilation failed in require at /Library/Perl/5.8.8/DBIx/Class/Schema/Loader/Base.pm line 292.

errors

• Turn on debugging

• Read error messages (sometimes useful!)

• Check field names

• Check package names

• Check which database you are connected to (development/test/live?) - repeat above

thanks

http://leo.cuckoo.org/projects/

Time for bonus slides?

Template Toolkit

• [% author.books.count %] not working?

• TT all methods are called in list context

• [% author.books_rs.count %] scalar context

Available for all relationships

Catalystpackage Your::App::Model::Bookstore;use base qw(Catalyst::Model::DBIC::Schema);

use strict;use warnings;

__PACKAGE__->config( schema_class => 'Bookstore::Schema',);

1;

Catalystpackage Your::App::Model::Bookstore;use base qw(Catalyst::Model::DBIC::Schema);

use strict;use warnings;

__PACKAGE__->config( schema_class => 'Bookstore::Schema',);

1;

Keep your Scheme in a separate package to your Catalyst application

Catalystsub action_name : Local { my ($self, $c) = @_;

my $model = $c->model('Bookstore'); my $author_model = $model->resultset('Authors'); }

1;

thanks!

http://leo.cuckoo.org/projects/