package Fey::Object::Table; BEGIN { $Fey::Object::Table::VERSION = '0.43'; } use strict; use warnings; use namespace::autoclean; use Fey::Literal::Function; use Fey::Placeholder; use Fey::SQL; use Fey::Table; use List::AllUtils qw( all ); use Object::ID qw( object_id ); use Scalar::Util qw( blessed ); use Try::Tiny; use Fey::Exceptions qw( param_error ); use Fey::ORM::Exceptions qw( no_such_row ); use Moose; override new => sub { my $class = shift; if ( $class->meta()->_object_cache_is_enabled() ) { my $instance = $class->meta()->_search_cache( ref $_[0] ? $_[0] : {@_} ); return $instance if $instance; } my $instance; my @args = @_; $class->_ClearConstructorError(); try { $instance = super(); } catch { die $_ unless blessed $_ && $_->isa('Fey::Exception::NoSuchRow'); $class->_SetConstructorError($_); }; return unless $instance; $class->meta()->_write_to_cache($instance) if $class->meta()->_object_cache_is_enabled(); return $instance; }; # I'd like to use MX::ClassAttribute but trying to apply this to each # Fey::ORM::Table-using class causes all sorts of weird errors. { my %E; sub ConstructorError { my $class = shift; return $E{$class}; } sub _SetConstructorError { my $class = shift; $E{$class} = shift; } sub _ClearConstructorError { my $class = shift; delete $E{$class}; } } sub BUILD { my $self = shift; my $p = shift; if ( delete $p->{_from_query} ) { $self->_require_pk($p); return; } $self->_load_from_dbms($p); return; } sub _require_pk { my $self = shift; my $p = shift; return if all { defined $p->{$_} } map { $_->name() } @{ $self->Table()->primary_key() }; my $package = ref $self; param_error "$package->new() requires that you pass the primary key if you set _from_query to true."; } sub EnableObjectCache { my $class = shift; $class->meta()->_set_object_cache_is_enabled(1); } sub DisableObjectCache { my $class = shift; $class->meta()->_set_object_cache_is_enabled(0); } sub ClearObjectCache { my $class = shift; $class->meta()->_clear_object_cache(); } sub _load_from_dbms { my $self = shift; my $p = shift; for my $key ( @{ $self->Table()->candidate_keys() } ) { my @names = map { $_->name() } @{$key}; next unless all { defined $p->{$_} } @names; return if $self->_load_from_key( $key, [ @{$p}{@names} ] ); } my $error = 'Could not find a row in ' . $self->Table()->name(); $error .= ' matching the values you provided to the constructor.'; no_such_row $error; } sub _load_from_key { my $self = shift; my $key = shift; my $bind = shift; my $select = $self->_SelectSQLForKey($key); return 1 if $self->_get_column_values( $select, $bind ); my $error = 'Could not find a row in ' . $self->Table()->name(); $error .= ' where '; my @where; for ( my $i = 0; $i < @{$key}; $i++ ) { push @where, $key->[$i]->name() . q{ = } . $bind->[$i]; } $error .= join ', ', @where; no_such_row $error; } # Based on discussions on #moose, this could be done more elegantly # with a custom instance metaclass that lazily initializes a batch of # attributes at once. sub _get_column_values { my $self = shift; my $select = shift; my $bind = shift; my $dbh = $self->_dbh($select); my $sth = $dbh->prepare( $self->_sql_string( $select, $dbh ) ); $sth->execute( @{$bind} ); my %col_values; $sth->bind_columns( \( @col_values{ @{ $sth->{NAME} } } ) ); my $fetched = $sth->fetch(); $sth->finish(); return unless $fetched; $self->_set_column_values_from_hashref( \%col_values ); return \%col_values; } sub _set_column_values_from_hashref { my $self = shift; my $values = shift; for my $col ( keys %{$values} ) { my $set = q{_set_} . $col; $self->$set( $values->{$col} ); } } sub _get_column_value { my $self = shift; my $col_values = $self->_get_column_values( $self->meta()->_select_by_pk_sql(), [ $self->pk_values_list() ], ); my $name = shift; return $col_values->{$name}; } sub pk_values_list { my $self = shift; my @cols = ( map { $_->name() } @{ $self->Table()->primary_key() } ); return map { $self->_deflated_value($_) } @cols; } sub _MakeSelectByPKSQL { my $class = shift; return $class->_SelectSQLForKey( $class->Table->primary_key() ); } sub _SelectSQLForKey { my $class = shift; my $key = shift; my $cache = $class->meta()->_select_sql_cache(); my $select = $cache->get($key); return $select if $select; my $table = $class->Table(); my @select = $table->columns(); $select = $class->SchemaClass()->SQLFactoryClass()->new_select(); $select->select( sort { $a->name() cmp $b->name() } @select ); $select->from($table); $select->where( $_, '=', Fey::Placeholder->new() ) for @{$key}; $cache->store( $key => $select ); return $select; } sub insert { my $class = shift; my %p = @_; return $class->insert_many( \%p ); } sub insert_many { my $class = shift; my @rows = @_; my $insert = $class->_insert_for_data( $rows[0] ); my $dbh = $class->_dbh($insert); my $sth = $dbh->prepare( $class->_sql_string( $insert, $dbh ) ); my @auto_inc_columns = ( grep { !exists $rows[0]->{$_} } map { $_->name() } grep { $_->is_auto_increment() } $class->Table->columns() ); my $table_name = $class->Table()->name(); my @non_literal_row_keys; my @literal_row_keys; my @ref_row_keys; for my $key ( sort keys %{ $rows[0] } ) { my $val = $rows[0]{$key}; if ( defined $val && blessed $val && $val->can('does') && ( $val->does('Fey::Role::IsLiteral') || $val->does('Fey::Role::SQL::ReturnsData') ) ) { push @literal_row_keys, $key; push @ref_row_keys, $key; } else { push @non_literal_row_keys, $key; push @ref_row_keys, $key if ref $val; } } my @bind_attributes = $class->_bind_attributes_for( $dbh, @non_literal_row_keys ); my $wantarray = wantarray; my @objects; for my $row (@rows) { push @objects, $class->_insert_one_row( $row, $dbh, $sth, \@non_literal_row_keys, \@ref_row_keys, \@bind_attributes, \@auto_inc_columns, $table_name, $wantarray, ); } return $wantarray ? @objects : $objects[0]; } sub _bind_attributes_for { my $self = shift; my $dbh = shift; my @keys = @_; return unless $dbh->{Driver}{Name} eq 'Pg'; my @attr = map { lc $self->Table()->column($_)->type() eq 'bytea' ? { pg_type => DBD::Pg::PG_BYTEA() } : {} } @keys; return unless grep { keys %{$_} } @attr; return @attr; } sub _insert_one_row { my $class = shift; # This is really grotesque my $row = shift; my $dbh = shift; my $sth = shift; my $non_literal_row_keys = shift; my $ref_row_keys = shift; my $bind_attributes = shift; my $auto_inc_columns = shift; my $table_name = shift; my $wantarray = shift; $class->_sth_execute( $sth, [ map { $class->_deflated_value( $_, $row->{$_} ) } @{$non_literal_row_keys} ], $bind_attributes, ); return unless defined $wantarray; my %auto_inc; for my $col ( @{$auto_inc_columns} ) { $auto_inc{$col} = $dbh->last_insert_id( undef, undef, $table_name, $col ); } delete @{$row}{ @{$ref_row_keys} } if @{$ref_row_keys}; return $class->new( %{$row}, %auto_inc, _from_query => 1 ); } sub _sth_execute { my $self = shift; my $sth = shift; my $vals = shift; my $attr = shift; if ( @{$attr} ) { for ( my $i = 0; $i < @{$vals}; $i++ ) { $sth->bind_param( $i + 1, $vals->[$i], $attr->[$i] ); } return $sth->execute(); } else { return $sth->execute( @{$vals} ); } } sub _deflated_value { my $self = shift; my $name = shift; my $val = @_ ? shift : $self->$name(); my $meth = $self->meta()->deflator_for($name); return $meth ? $self->$meth($val) : $val; } sub _insert_for_data { my $class = shift; my $data = shift; my $insert = $class->SchemaClass()->SQLFactoryClass()->new_insert(); my $table = $class->Table(); $insert->into( $table->columns( sort keys %{$data} ) ); my $ph = Fey::Placeholder->new(); my @vals = ( map { $_ => ( defined $data->{$_} && blessed $data->{$_} && $data->{$_}->can('does') && ( $data->{$_}->does('Fey::Role::IsLiteral') || $data->{$_}->does('Fey::Role::SQL::ReturnsData') ) ? $data->{$_} : $ph ) } sort keys %{$data} ); $insert->values(@vals); return $insert; } sub update { my $self = shift; my %p = @_; my $update = $self->SchemaClass()->SQLFactoryClass()->new_update(); my $table = $self->Table(); $update->update($table); $update->set( map { $table->column($_) => $self->_deflated_value( $_, $p{$_} ) } sort keys %p ); for my $col ( @{ $table->primary_key() } ) { my $name = $col->name(); $update->where( $col, '=', $self->_deflated_value($name) ); } my $dbh = $self->_dbh($update); my $sth = $dbh->prepare( $self->_sql_string( $update, $dbh ) ); my @attr = $self->_bind_attributes_for( $dbh, ( sort keys %p, map { $_->name() } @{ $table->primary_key() } ), ); $self->_sth_execute( $sth, [ $update->bind_params() ], \@attr ); for my $k ( sort keys %p ) { if ( ref $p{$k} ) { my $clear = q{_clear_} . $k; $self->$clear(); } else { my $set = q{_set_} . $k; $self->$set( $p{$k} ); } } return; } sub delete { my $self = shift; my $delete = $self->SchemaClass()->SQLFactoryClass()->new_delete(); my $table = $self->Table(); $delete->from($table); for my $col ( @{ $table->primary_key() } ) { my $name = $col->name(); $delete->where( $col, '=', $self->_deflated_value($name) ); } my $dbh = $self->_dbh($delete); $dbh->do( $self->_sql_string( $delete, $dbh ), {}, $delete->bind_params() ); return; } sub _dbh { my $self = shift; my $sql = shift; my $source = $self->SchemaClass()->DBIManager()->source_for_sql($sql); die "Could not get a source for this sql ($sql)" unless $source; return $source->dbh(); } sub pk_values_hash { my $self = shift; my @vals = $self->pk_values_list() or return; my @cols = ( map { $_->name() } @{ $self->Table()->primary_key() } ); return map { $cols[$_] => $vals[$_] } 0 .. $#vals; } sub Count { my $class = shift; my $select = $class->meta()->_count_sql(); my $dbh = $class->_dbh($select); my $row = $dbh->selectcol_arrayref( $class->_sql_string( $select, $dbh ) ); return $row->[0]; } sub Table { my $class = shift; return $class->meta()->table(); } sub SchemaClass { my $class = shift; return $class->meta()->schema_class(); } sub _sql_string { my $self = shift; my $sql = shift; my $dbh = shift; my $cache = $self->meta()->_sql_string_cache(); return $cache->{ object_id($sql) . object_id($dbh) } ||= $sql->sql($dbh); } __PACKAGE__->meta()->make_immutable( inline_constructor => 0 ); 1; # ABSTRACT: Base class for table-based objects =pod =head1 NAME Fey::Object::Table - Base class for table-based objects =head1 VERSION version 0.43 =head1 SYNOPSIS package MyApp::User; use Fey::ORM::Table; has_table(...); =head1 DESCRIPTION This class is a the base class for all table-based objects. It implements a large amount of the core L functionality, including CRUD (create, update, delete) and loading of data from the DBMS. =head1 METHODS This class provides the following methods: =head2 $class->new(...) This method overrides the default C constructor in order to implement cache management. By default, object caching is disabled. In that case, this method lets its parent class do most of the work. However, unlike the standard Moose constructor, this method may sometimes not return an object. If it attempts to load object data from the DBMS and cannot find anything matching the parameters given to the constructor, it will return false. If the constructor fails, you can check the value of C<< $class->ConstructorError >> for the error message. This is done so that calling the constructor does not overwrite any value already in C<$@>. If caching is enabled, then this method will attempt to find a matching object in the cache. A match is determined by looking for an object which has a candidate key with the same values as are passed to the constructor. If no match is found, it attempts to create a new object. If this succeeds, it stores it in the cache before returning it. =head3 Constructor Parameters The constructor accepts any attribute of the class as a parameter. This includes any column-based attributes, as well as any additional attributes defined by C or C. Of course, if you disabled caching for C or C relationships, then they are implemented as simple methods, not attributes. If you define additional methods via Moose's C function, and these will be accepted by the constructor as well. Finally, the constructor accepts a parameter C<_from_query>. This tells the constructor that the parameters passed to the constructor are the result of a C statement and then pass the statement and bind parameters to C<< $object->_get_column_values() >>. On success, this method should simply return. If it fails, it should throw a L exception. See L for details. =head2 $object->_get_column_values( $select, $bind_params ) This method takes a C is expected to find a single row, which should correspond to the current object. If it finds a row, it sets the corresponding attributes in the object based on the values returns by the C