678 lines
13 KiB
Plaintext
678 lines
13 KiB
Plaintext
=head1 NAME
|
|
|
|
DBIx::Class::Manual::Features - A boatload of DBIx::Class features with links to respective documentation
|
|
|
|
=head1 META
|
|
|
|
=head2 Large Community
|
|
|
|
There are L<hundres of DBIC contributors|DBIx::Class/AUTHORS> listed in
|
|
F<AUTHORS>. That ranges from documentation help, to test help, to added
|
|
features, to entire database support.
|
|
|
|
=head2 Active Community
|
|
|
|
Currently (June 9, 2010) 6 active branches (committed to
|
|
in the last two weeks) in git. Last release (0.08122)
|
|
had 14 new features, and 16 bug fixes. Of course that
|
|
L<ebbs and flows|https://metacpan.org/changes/distribution/DBIx-Class>.)
|
|
|
|
=head2 Responsive Community
|
|
|
|
=over 1
|
|
|
|
=item I needed MSSQL order-by support; the community helped me add support
|
|
|
|
=item generally very welcoming of people willing to help
|
|
|
|
=back
|
|
|
|
=head1 General ORM
|
|
|
|
These are things that are in most other ORMs, but are still reasons to use
|
|
DBIC over raw SQL.
|
|
|
|
=head2 Cross DB
|
|
|
|
The vast majority of code should run on all databases without needing tweaking
|
|
|
|
=head2 Basic CRUD
|
|
|
|
=over 1
|
|
|
|
=item C - Create
|
|
|
|
=item R - Retrieve
|
|
|
|
=item U - Update
|
|
|
|
=item D - Delete
|
|
|
|
=back
|
|
|
|
=head2 SQL: Create
|
|
|
|
my $sth = $dbh->prepare('
|
|
INSERT INTO books
|
|
(title, author_id)
|
|
values (?,?)
|
|
');
|
|
|
|
$sth->execute( 'A book title', $author_id );
|
|
|
|
=head2 DBIC: Create
|
|
|
|
my $book = $book_rs->create({
|
|
title => 'A book title',
|
|
author_id => $author_id,
|
|
});
|
|
|
|
See L<DBIx::Class::ResultSet/create>
|
|
|
|
=over 1
|
|
|
|
=item No need to pair placeholders and values
|
|
|
|
=item Automatically gets autoincremented id for you
|
|
|
|
=item Transparently uses INSERT ... RETURNING for databases that support it
|
|
|
|
=back
|
|
|
|
=head2 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 ) {
|
|
say "Author of $book->{title} is $book->{author_name}";
|
|
}
|
|
|
|
=head2 DBIC: Read
|
|
|
|
my $book = $book_rs->find($book_id);
|
|
|
|
or
|
|
|
|
my $book = $book_rs->search({ title => 'A book title' }, { rows => 1 })->next;
|
|
|
|
or
|
|
|
|
my @books = $book_rs->search({ author => $author_id })->all;
|
|
|
|
or
|
|
|
|
while( my $book = $books_rs->next ) {
|
|
printf "Author of %s is %s\n", $book->title, $book->author->name;
|
|
}
|
|
|
|
See L<DBIx::Class::ResultSet/find>, L<DBIx::Class::ResultSet/search>, L<DBIx::Class::ResultSet/next>, and L<DBIx::Class::ResultSet/all>
|
|
|
|
B<TMTOWTDI!>
|
|
|
|
=head2 SQL: Update
|
|
|
|
my $update = $dbh->prepare('
|
|
UPDATE books
|
|
SET title = ?
|
|
WHERE id = ?
|
|
');
|
|
|
|
$update->execute( 'New title', $book_id );
|
|
|
|
=head2 DBIC: Update
|
|
|
|
$book->update({ title => 'New title' });
|
|
|
|
See L<DBIx::Class::Row/update>
|
|
|
|
Will not update unless value changes
|
|
|
|
=head2 SQL: Delete
|
|
|
|
my $delete = $dbh->prepare('DELETE FROM books WHERE id = ?');
|
|
|
|
$delete->execute($book_id);
|
|
|
|
=head2 DBIC: Delete
|
|
|
|
$book->delete
|
|
|
|
See L<DBIx::Class::Row/delete>
|
|
|
|
=head2 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"
|
|
');
|
|
|
|
=head2 DBIC: Search
|
|
|
|
my $book = $book_rs->search({
|
|
'me.name' => { -like => '%monte cristo%' },
|
|
'me.topic' => 'jailbreak',
|
|
})->next;
|
|
|
|
=over 1
|
|
|
|
=item See L<SQL::Abstract::Classic>, L<DBIx::Class::ResultSet/next>, and L<DBIx::Class::ResultSet/search>
|
|
|
|
=item (kinda) introspectible
|
|
|
|
=item Prettier than SQL
|
|
|
|
=back
|
|
|
|
=head2 OO Overridability
|
|
|
|
=over 1
|
|
|
|
=item Override new if you want to do validation
|
|
|
|
=item Override delete if you want to disable deletion
|
|
|
|
=item and on and on
|
|
|
|
=back
|
|
|
|
=head2 Convenience Methods
|
|
|
|
=over 1
|
|
|
|
=item L<DBIx::Class::ResultSet/find_or_create>
|
|
|
|
=item L<DBIx::Class::ResultSet/update_or_create>
|
|
|
|
=back
|
|
|
|
=head2 Non-column methods
|
|
|
|
Need a method to get a user's gravatar URL? Add a C<gravatar_url> method to the
|
|
Result class
|
|
|
|
=head2 RELATIONSHIPS
|
|
|
|
=over 1
|
|
|
|
=item L<DBIx::Class::Relationship/belongs_to>
|
|
|
|
=item L<DBIx::Class::Relationship/has_many>
|
|
|
|
=item L<DBIx::Class::Relationship/might_have>
|
|
|
|
=item L<DBIx::Class::Relationship/has_one>
|
|
|
|
=item L<DBIx::Class::Relationship/many_to_many>
|
|
|
|
=item SET AND FORGET
|
|
|
|
=back
|
|
|
|
=head1 DBIx::Class Specific Features
|
|
|
|
These things may be in other ORM's, but they are very specific, so doubtful
|
|
|
|
=head2 ->deploy
|
|
|
|
Create a database from your DBIx::Class schema.
|
|
|
|
my $schema = Frew::Schema->connect( $dsn, $user, $pass );
|
|
|
|
$schema->deploy
|
|
|
|
See L<DBIx::Class::Schema/deploy>.
|
|
|
|
See also: L<DBIx::Class::DeploymentHandler>
|
|
|
|
=head2 Schema::Loader
|
|
|
|
Create a DBIx::Class schema from your database.
|
|
|
|
package Frew::Schema;
|
|
|
|
use strict;
|
|
use warnings;
|
|
|
|
use base 'DBIx::Class::Schema::Loader';
|
|
|
|
__PACKAGE__->loader_options({
|
|
naming => 'v7',
|
|
debug => $ENV{DBIC_TRACE},
|
|
});
|
|
|
|
1;
|
|
|
|
# elsewhere...
|
|
|
|
my $schema = Frew::Schema->connect( $dsn, $user, $pass );
|
|
|
|
See L<DBIx::Class::Schema::Loader> and L<DBIx::Class::Schema::Loader::Base/CONSTRUCTOR OPTIONS>.
|
|
|
|
=head2 Populate
|
|
|
|
Made for inserting lots of rows very quickly into database
|
|
|
|
$schema->populate([ Users =>
|
|
[qw( username password )],
|
|
[qw( frew >=4char$ )],
|
|
[qw( ... )],
|
|
[qw( ... )],
|
|
);
|
|
|
|
See L<DBIx::Class::Schema/populate>
|
|
|
|
I use populate L<here|http://blog.afoolishmanifesto.com/archives/1255> to export our whole
|
|
(200M~) db to SQLite
|
|
|
|
=head2 Multicreate
|
|
|
|
Create an object and its 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' },
|
|
},
|
|
});
|
|
|
|
See L<DBIx::Class::ResultSet/create>
|
|
|
|
=over 1
|
|
|
|
=item books is a has_many
|
|
|
|
=item address is a belongs_to which in turn belongs to state and city each
|
|
|
|
=item for this to work right state and city must mark abbreviation and name as unique
|
|
|
|
=back
|
|
|
|
=head2 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.
|
|
|
|
=head2 Extensibility example: DBIx::Class::Helpers
|
|
|
|
=over 1
|
|
|
|
=item L<DBIx::Class::Helper::ResultSet::IgnoreWantarray>
|
|
|
|
=item L<DBIx::Class::Helper::ResultSet::Random>
|
|
|
|
=item L<DBIx::Class::Helper::ResultSet::SetOperations>
|
|
|
|
=item L<DBIx::Class::Helper::Row::JoinTable>
|
|
|
|
=item L<DBIx::Class::Helper::Row::NumifyGet>
|
|
|
|
=item L<DBIx::Class::Helper::Row::SubClass>
|
|
|
|
=item L<DBIx::Class::Helper::Row::ToJSON>
|
|
|
|
=item L<DBIx::Class::Helper::Row::StorageValues>
|
|
|
|
=item L<DBIx::Class::Helper::Row::OnColumnChange>
|
|
|
|
=back
|
|
|
|
=head2 Extensibility example: DBIx::Class::TimeStamp
|
|
|
|
=over 1
|
|
|
|
=item See L<DBIx::Class::TimeStamp>
|
|
|
|
=item Cross DB
|
|
|
|
=item set_on_create
|
|
|
|
=item set_on_update
|
|
|
|
=back
|
|
|
|
=head2 Extensibility example: Kioku
|
|
|
|
=over 1
|
|
|
|
=item See L<DBIx::Class::Schema::KiokuDB>
|
|
|
|
=item Kioku is the new hotness
|
|
|
|
=item Mix RDBMS with Object DB
|
|
|
|
=back
|
|
|
|
=head2 Result vs ResultSet
|
|
|
|
=over 1
|
|
|
|
=item Result == Row
|
|
|
|
=item ResultSet == Query Plan
|
|
|
|
=over 1
|
|
|
|
=item Internal Join Optimizer for all DB's (!!!)
|
|
|
|
=back
|
|
|
|
=item (less important but...)
|
|
|
|
=item ResultSource == Queryable collection of rows (Table, View, etc)
|
|
|
|
=item Storage == Database
|
|
|
|
=item Schema == associates a set of ResultSources with a Storage
|
|
|
|
=back
|
|
|
|
=head2 ResultSet methods
|
|
|
|
package MyApp::Schema::ResultSet::Book;
|
|
|
|
use strict;
|
|
use warnings;
|
|
|
|
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}
|
|
})
|
|
};
|
|
|
|
# ...
|
|
|
|
1;
|
|
|
|
See L<DBIx::Class::Manual::Cookbook/Predefined searches>
|
|
|
|
=over 1
|
|
|
|
=item All searches should be ResultSet methods
|
|
|
|
=item Name has obvious meaning
|
|
|
|
=item L<DBIx::Class::ResultSet/current_source_alias> helps things to work no matter what
|
|
|
|
=back
|
|
|
|
=head2 ResultSet method in Action
|
|
|
|
$schema->resultset('Book')->good
|
|
|
|
=head2 ResultSet Chaining
|
|
|
|
$schema->resultset('Book')
|
|
->good
|
|
->cheap
|
|
->recent
|
|
|
|
=head2 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')
|
|
->search(undef, { rows => 1})
|
|
->get_column('raw_scores')
|
|
->next;
|
|
|
|
The SQL that this produces (with placeholders filled in for clarity's sake)
|
|
on our system (Microsoft SQL) is:
|
|
|
|
SELECT raw_scores
|
|
FROM (
|
|
SELECT raw_scores, ROW_NUMBER() OVER (
|
|
ORDER BY (
|
|
SELECT (1)
|
|
)
|
|
) AS rno__row__index
|
|
FROM (
|
|
SELECT rpt_score.raw_scores
|
|
FROM users me
|
|
JOIN access access
|
|
ON access.userid = me.userid
|
|
JOIN mgmt mgmt
|
|
ON mgmt.mgmtid = access.mgmtid
|
|
JOIN [order] orders
|
|
ON orders.mgmtid = mgmt.mgmtid
|
|
JOIN shop shops
|
|
ON shops.orderno = orders.orderno
|
|
JOIN rpt_scores rpt_score
|
|
ON rpt_score.shopno = shops.shopno
|
|
WHERE (
|
|
datecompleted IS NOT NULL AND
|
|
(
|
|
(shops.datecompleted BETWEEN '2009-10-01' AND '2009-10-08') AND
|
|
(type = '1' AND me.userid = 'frew')
|
|
)
|
|
)
|
|
) rpt_score
|
|
) rpt_score
|
|
WHERE rno__row__index BETWEEN 1 AND 1
|
|
|
|
See: L<DBIx::Class::ResultSet/related_resultset>,
|
|
L<DBIx::Class::ResultSet/search_related>, and
|
|
L<DBIx::Class::ResultSet/get_column>.
|
|
|
|
=head2 bonus rel methods
|
|
|
|
my $book = $author->create_related(
|
|
books => {
|
|
title => 'Another Discworld book',
|
|
}
|
|
);
|
|
|
|
my $book2 = $pratchett->add_to_books({
|
|
title => 'MOAR Discworld book',
|
|
});
|
|
|
|
See L<DBIx::Class::Relationship::Base/create_related> and L<DBIx::Class::Relationship::Base/add_to_$rel>
|
|
|
|
Note that it automatically fills in foreign key for you
|
|
|
|
=head2 Excellent Transaction Support
|
|
|
|
$schema->txn_do(sub {
|
|
...
|
|
});
|
|
|
|
$schema->txn_begin; # <-- low level
|
|
# ...
|
|
$schema->txn_commit;
|
|
|
|
See L<DBIx::Class::Schema/txn_do>, L<DBIx::Class::Schema/txn_begin>,
|
|
and L<DBIx::Class::Schema/txn_commit>.
|
|
|
|
=head2 InflateColumn
|
|
|
|
package Frew::Schema::Result::Book;
|
|
|
|
use strict;
|
|
use warnings;
|
|
|
|
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 },
|
|
},
|
|
);
|
|
|
|
See L<DBIx::Class::InflateColumn>, L<DBIx::Class::InflateColumn/inflate_column>, and
|
|
L<DBIx::Class::InflateColumn::DateTime>.
|
|
|
|
=head2 InflateColumn: deflation
|
|
|
|
$book->date_published(DateTime->now);
|
|
$book->update;
|
|
|
|
=head2 InflateColumn: inflation
|
|
|
|
say $book->date_published->month_abbr; # Nov
|
|
|
|
=head2 FilterColumn
|
|
|
|
package Frew::Schema::Result::Book;
|
|
|
|
use strict;
|
|
use warnings;
|
|
|
|
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 }
|
|
|
|
See L<DBIx::Class::FilterColumn> and L<DBIx::Class::FilterColumn/filter_column>
|
|
|
|
=head2 ResultSetColumn
|
|
|
|
my $rsc = $schema->resultset('Book')->get_column('price');
|
|
$rsc->first;
|
|
$rsc->all;
|
|
$rsc->min;
|
|
$rsc->max;
|
|
$rsc->sum;
|
|
|
|
See L<DBIx::Class::ResultSetColumn>
|
|
|
|
=head2 Aggregates
|
|
|
|
my @res = $rs->search(undef, {
|
|
select => [
|
|
'price',
|
|
'genre',
|
|
{ max => price },
|
|
{ avg => price },
|
|
],
|
|
as => [
|
|
qw(price genre max_price avg_price)
|
|
],
|
|
group_by => [qw(price genre)],
|
|
});
|
|
for (@res) {
|
|
say $_->price . ' ' . $_->genre;
|
|
say $_->get_column('max_price');
|
|
say $_->get_column('avg_price');
|
|
}
|
|
|
|
See L<DBIx::Class::ResultSet/select>, L<DBIx::Class::ResultSet/as>, and
|
|
L<DBIx::Class::ResultSet/group_by>
|
|
|
|
=over 1
|
|
|
|
=item Careful, get_column can basically mean B<three> things
|
|
|
|
=item private in which case you should use an accessor
|
|
|
|
=item public for what there is no accessor for
|
|
|
|
=item public for get resultset column (prev example)
|
|
|
|
=back
|
|
|
|
=head2 HRI
|
|
|
|
$rs->search(undef, {
|
|
result_class => 'DBIx::Class::ResultClass::HashRefInflator',
|
|
});
|
|
|
|
See L<DBIx::Class::ResultSet/result_class> and L<DBIx::Class::ResultClass::HashRefInflator>.
|
|
|
|
=over 1
|
|
|
|
=item Easy on memory
|
|
|
|
=item Mega fast
|
|
|
|
=item Great for quick debugging
|
|
|
|
=item Great for performance tuning (we went from 2m to < 3s)
|
|
|
|
=back
|
|
|
|
=head2 Subquery Support
|
|
|
|
my $inner_query = $schema->resultset('Artist')
|
|
->search({
|
|
name => [ 'Billy Joel', 'Brittany Spears' ],
|
|
})->get_column('id')->as_query;
|
|
|
|
my $rs = $schema->resultset('CD')->search({
|
|
artist_id => { -in => $inner_query },
|
|
});
|
|
|
|
See L<DBIx::Class::Manual::Cookbook/Subqueries>
|
|
|
|
=head2 Bare SQL w/ Placeholders
|
|
|
|
$rs->update({
|
|
# !!! SQL INJECTION VECTOR
|
|
price => \"price + $inc", # DON'T DO THIS
|
|
});
|
|
|
|
Better:
|
|
|
|
$rs->update({
|
|
price => \['price + ?', [inc => $inc]],
|
|
});
|
|
|
|
See L<SQL::Abstract::Classic/Literal SQL with placeholders and bind values (subqueries)>
|
|
|
|
=head1 FURTHER QUESTIONS?
|
|
|
|
Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
|
|
|
|
=head1 COPYRIGHT AND LICENSE
|
|
|
|
This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
|
|
by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
|
|
redistribute it and/or modify it under the same terms as the
|
|
L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
|