Fey, Another Freakin' ORM

Dave Rolsky

Why another ORM?

Fey anatomy

SQL generation with Perl

SQL Functions

my $avg_track_length =
    Fey::Literal::Function->new(
        'AVG', $track_t->column('length')
    );

my $select = Fey::SQL->new_select();

Select

$select
  ->select( $track_t )
  # figures out appropriate join
  ->from( $album_t, $track_t )
  ->where( $album_t->column('year'), '>',
           Fey::Placeholder->new() )
  ->and( $track_t->column('length'), '>',
         $avg_track_length )
  ->order_by( $album_t->column('name'),
              $track_t->column('name') );

# quotes the crap out of everything
my $sth = $dbh->prepare( $select->sql($dbh) );
$sth->execute( 1990 );

Auto-placeholders

my $select = Fey::SQL->new_select();

$select->select( $track_t )
       # figures out appropriate join
       ->from( $album_t, $track_t )
       ->where( $album_t->column('year'),
                   '=', 2005 );
       ->and( $album_t->column('artist_id'), '=',
              $tommy_heavenly6->artist_id() );

my $sth = $dbh->prepare( $select->sql($dbh) );
$sth->execute( $select->bind_params() );

Fey core philosophy

Why use it?

Support Any Query

FROM TableA
     LEFT OUTER JOIN TableB USING (foo_id)
     WHERE TableB.size > 10

Be Helpful

Fey::Loader

my $loader = Fey::Loader->new( dbh => $dbh );
my $schema = $loader->make_schema();

Fey::Loader

Fey::Loader

Schema Introspection

my $loader = Fey::Loader->new( dbh => $dbh );
my $schema = $loader->make_schema();

for my $table ( $schema->tables() ) {
    print $table->name(), "\n";

    for my $column ( $table->columns() ) {
        print $column->name(), "\n";
    }
}

Logical Schema Only

Fey::DBIManager

Fey::DBIManager

Fey::DBIManager example

package MyApp::Schema;

my $source =
    Fey::DBIManager::Source->new(
        dsn          => ...,
        username     => ...,
        password     => ...,
        post_connect => \&_set_dbh_attributes,
    );

sub _set_dbh_attributes {
    my $dbh = shift;

    $dbh->{pg_enable_utf8} = 1;
    $dbh->do( 'SET TIME ZONE UTC' );
}

Fey::DBIManager post-connect

Fey::ORM

Fey::ORM is SQLy

Fey::ORM is Moosey

Fey::ORM - MyApp::Schema

package MyApp::Schema;

use Fey::ORM::Schema;

{
    my $source = Fey::DBIManager::Source->new( ... );

    my $schema =
        Fey::Loader->new( dbh => $source->dbh() )
                   ->make_schema();

    has_schema $schema;

    __PACKAGE__->DBIManager()->add_source($source);
}

MyApp::Schema::Album

package MyApp::Schema::Album;
use MyApp::Schema;
use Fey::ORM::Table;

{
    my $schema = MyApp::Schema->Schema();

    has_table( $schema->table('Album') );

    has_one( $schema->table('Artist') );

    has_many 'tracks' => (
        table    => $schema->table('Track'),
        order_by =>
            [ $schema->table('Track') 
                     ->column('track_number') ],
        cache    => 1,
    );
}

MyApp::Schema::Album

package MyApp::Schema::Album;

transform 'release_date'
    => deflate { blessed $_[1]
                 ? DateTime::Format::Pg
                       ->format_datetime( $_[1] )
                 : $_[1] }
    => inflate { DateTime::Format::Pg
                     ->parse_datetime( $_[1] ) };

MyApp::Schema::Album

package MyApp::Schema::Album;

has_many 'artists' => (
    table       => $schema->table('Artist'),
    # SELECT Artist.*
    # FROM Track JOIN Artist USING (artist_id)
    # WHERE Track.album_id = ?
    select      => ..., # Fey::SQL::Select object
    bind_params => sub { $_[0]->album_id() },
    cache       => 1,
);

MyApp::Schema::Album

has 'track_count' => (
    metaclass   => 'FromSelect',
    select      => $select,
    bind_params =>
        sub { $_[0]->album_id() },
);

Constructors

my $album =
    MyApp::Schema::Album->new(
        album_id => 42 );

Constructors

Constructors

sub _load_from_dbms {
    my $self = shift;
    my $p    = shift;

    return unless exists $p->{password};

    my $select = $self->_select_by_credentials();

    return if
        $self->_get_column_values(
            $select,
            [ $p->{username}, sha512( $p->{password} ) ],
        );

    die 'Invalid username or password';
}

Insert, Update, Delete


my $album = MyApp::Schema::Album->insert(
                title     => 'Realism',
                year      => 2009,
                artist_id => 42,
            );

$album->update( year => 2010 );

$album->delete();

Fey::ORM::Table + Moose

package MyApp::Schema::Album;

use Fey::ORM::Table;

# Plain old Moose attributes
has 'reverse_title' => (
    is      => 'ro',
    isa     => 'Str',
    default => sub { reverse $_[0]->title() },
);

# Plain old Moose roles
with 'MyApp::Role::MusicCollection;

# Plain old Moose method modifiers
before 'update' => sub { ... };

Fey::Object::Iterator

Custom Select

my $select = MyApp::Schema->SQLFactoryClass()->new_select();

my $schema = MyApp::Schema->Schema();

my $track_t = $schema->table('Track');

$select->select($track_t)
       ->where( $track_t->column('album_id'),
                '=', $self->album_id() )
       ->and  ( $track_t->column('has_been_played'),
                '=', 0 )
       ->order_by( $track_t->column('title') );

Custom Select

package MyApp::Schema::Album;

sub unheard_tracks {
    my $self = shift;

    my $select = ...;

    my $dbh =
        MyApp::Schema->DBIManager()
                     ->source_for_sql($select)->dbh();

    return Fey::Object::Iterator::FromSelect->new(
        classes => ['MyApp::Schema::Track'],
        select  => $select,
        dbh => $dbh,
        bind_params => [ $select->bind_params() ],
    );
}

Object Caching

MyApp::Schema->EnableObjectCaches();

my $user1 = MyApp::Schema::User->new( user_id => 42 );
my $user2 = MyApp::Schema::User->new( user_id => 42 );

MyApp::Schema->ClearObjectCaches();

my $user3 = MyApp::Schema::User->new( user_id => 42 );

Transactions

my $trans = sub {
    $user->delete();

    MyApp::Schema::Log->insert( message => 'deleted user' );
};

MyApp::Schema->RunInTransaction($trans);

More Fey::ORM features

Fey::ORM MOP

Fey::ORM is not done!

Fey::ORM::Mock

Data seeding

use MyApp::Schema;

my $mock = Fey::ORM::Mock->new(
               schema_class => 'MyApp::Schema' )

$mock->seed_class(
    'MyApp::Schema::Album' =>
        { title  => 'Get Lost',
          artist => 'Magnetic Fields',
        },
        { ... },
);

# next select will see this data

Tracking insert/update/delete

use MyApp::Schema;

my $mock = Fey::ORM::Mock->new(
               schema_class => 'MyApp::Schema' );

my $album = MyApp::Schema::Album->insert(...);
$album->delete();

my $history =
    $mock->recorder()->actions_for('MyApp::Schema::Album');

Learning More

Thank you