skip tests unless DBD::SQLite installed
[blog.pm-common-perl-mods.git] / Rose-DBx-Object-I18N / lib / Rose / DBx / Object / I18N.pm
blob64582b44047191bb6a785aca95bd45f546163da2
1 package Rose::DBx::Object::I18N;
3 use base qw/ Rose::DB::Object /;
4 require Carp;
6 use Hash::Merge 'merge';
8 use Rose::DB::Object::Constants qw(:all);
9 use Rose::DB::Constants qw(IN_TRANSACTION);
11 use Rose::DB::Object::Helpers qw/ has_loaded_related /;
13 use Rose::DBx::Object::I18N::Helpers ':all';
15 our $Debug = 0;
17 our $VERSION = 0.04;
19 =head1 NAME
21 Rose::DBx::Object::I18N - set of modules to deal with multilingual database
23 =head1 SYNOPSIS
25 # create user with multilingual data
26 my $u = User->new(
27 name => 'ppp',
28 orig_lang => 'en',
29 signature => 'hello'
31 $u->save();
33 # load german translation
34 $u->load( i18n => 'de' );
35 $u->signature; # hello
37 # retrieve available translations
38 $u->i18n_available_translations; # undef
40 # update translation
41 $u->i18n->signature( 'hallo' );
42 $u->save();
43 $u->i18n_available_translations; # [ 'en' ]
45 # update original
46 $u->i18n( 'en' )->signature( 'hi' );
47 $u->save();
49 # check if original translation is loaded
50 $u->is_original_loaded; # 1
52 $u->i18n( 'de' );
54 # delete loaded translation
55 $u->delete_i18n();
56 $u->i18n_available_translations; # undef
57 $u->i18n( 'de' )->signature; # hi
59 =head1 DESCRIPTION
61 There are different ways to deal with multilingual problem. We will look at a
62 few of them.
64 =head2 Separate Data For Each Language
66 articles
67 +----+-----------+----------+-------+
68 | id | author_id | language | title |
69 +----+-----------+----------+-------+
70 | 1 | 1 | en | foo |
71 +----+-----------+----------+-------+
72 | 2 | 1 | de | bar |
73 +----+-----------+----------+-------+
74 | 3 | 2 | en | foo |
75 +----+-----------+----------+-------+
77 This is a easiest one to imagine. You have all data separated. If user wants
78 something in English just give him what he wants. There is no relation between
79 data, so if nothing is found in English there is no way how to know if there is
80 something in German, etc.
82 Also, the data that is shared between translations, like link, author id,
83 something else that can't be translated should be synchronized on every change
84 in other translations.
86 The good is the speed. No joins, no lookups in other tables, etc.
88 =head2 Static Data, Language Data, Translation Data
90 articles
91 +----+-----------+-------------------+
92 | id | author_id | original_language |
93 +----+-----------+-------------------+
94 | 1 | 1 | en |
95 +----+-----------+-------------------+
96 | 2 | 2 | de |
97 +----+-----------+-------------------+
99 languages
100 +------------+---------+----------+
101 | article_id | i18n_id | language |
102 +------------+---------+----------+
103 | 1 | 1 | en |
104 +------------+---------+----------+
105 | 1 | 2 | de |
106 +------------+---------+----------+
107 | 2 | 3 | de |
108 +------------+---------+----------+
110 i18n
111 +----+-------+
112 | id | title |
113 +----+-------+
114 | 1 | foo |
115 +----+-------+
116 | 2 | bar |
117 +----+-------+
118 | 3 | foo |
119 +----+-------+
121 Here we have three tables. One is for static data that is not going to be
122 translated, one is for languages that will hold what language is mapped to what
123 translation in translations table and the translation table, that holds
124 translatable information.
126 The problem is that there too many thins to do even for the one request. We
127 should make 3 joins and one IF statement in a join.
129 =head2 One Static, Many Translations
131 articles
132 +----+-----------+-------------------+
133 | id | author_id | original_language |
134 +----+-----------+-------------------+
135 | 1 | 1 | en |
136 +----+-----------+-------------------+
137 | 2 | 2 | de |
138 +----+-----------+-------------------+
140 i18n
141 +------------+----------+-------+
142 | article_id | language | title |
143 +------------+----------+-------+
144 | 1 | en | foo |
145 +------------+----------+-------+
146 | 1 | de | bar |
147 +------------+----------+-------+
148 | 2 | de | foo |
149 +------------+----------+-------+
151 Current approach for Rose::DBx::Object::I18N is to have two tables, one is for
152 the static data, and another for all translations.
154 =head2 Rose::DBx::Object::I18N
156 Plugging in Rose::DBx::Object::I18N is simply, instead of subclassing from
157 Rose::DB::Object use this namespace. But you must have two tables: one for the
158 Static data and another for Translation data.
160 package DB::Object::I18N;
162 use strict;
164 use base qw/ Rose::DBx::Object::I18N / ;
166 use DB;
168 sub init_db {
169 my $self = shift;
171 DB->new_or_cached( @_ );
174 sub i18n_languages {
175 my @languages = qw/ en de ru /;
177 wantarray ? @languages : \@languages;
180 Class for Static data can look like this.
182 package User;
184 use strict;
186 use base qw(DB::Object::I18N::Static);
188 use Rose::DBx::Object::I18N::Metadata;
189 sub meta_class { 'Rose::DBx::Object::I18N::Metadata' };
191 __PACKAGE__->meta->setup(
192 table => 'user',
194 columns => [
195 qw/ id name /,
196 orig_lang => { type => 'i18n_language' }
199 primary_key_columns => [ qw/ id / ],
201 unique_key => [ qw/ name / ],
203 relationships => [
204 user_i18n => {
205 type => 'one to many',
206 class => 'UserI18N',
207 column_map => { id => 'user_id' }
211 i18n_translation_rel_name => 'user_i18n'
214 And class for Translation
216 package UserI18N;
218 use strict;
220 use base qw/ DB::Object::I18N::Translation /;
222 use Rose::DBx::Object::I18N::Metadata;
223 sub meta_class { 'Rose::DBx::Object::I18N::Metadata' };
225 __PACKAGE__->meta->setup(
226 table => 'user_i18n',
228 columns => [
230 i18nid
231 user_id
232 signature
234 lang => { type => 'i18n_language' },
235 istran => { type => 'i18n_is_translation' }
238 primary_key_columns => [ 'i18nid' ],
240 foreign_keys => [
241 user => {
242 class => 'User',
243 key_columns => { user_id => 'id' },
244 rel_type => 'many to one',
248 i18n_static_rel_name => 'user'
251 There is also I18N::Manager that can help you with selection i18n data.
253 package User::Manager;
255 use strict;
257 use base 'Rose::DBx::Object::I18N::Manager';
259 sub object_class { 'User' }
261 __PACKAGE__->make_manager_methods( 'users' );
263 =head1 METHODS
265 =head2 new
267 Rose::DB::Object init method is overloaded, so you can use one of these
268 examples:
270 my $u = User->new(
271 name => 'vti',
272 orig_lang => 'en',
273 user_i18n => { signature => 'hello' }
278 my $u = User->new(
279 name => 'fake',
280 orig_lang => 'en',
281 signature => 'hello'
284 or even
286 my $u = User->new(
287 name => 'foo',
288 orig_lang => 'en'
291 and then
293 $u->user_i18n( { signature => 'hello' } );
295 =cut
297 sub init {
298 my ( $self ) = shift;
300 my %params = @_;
302 if ( my $rel_name = $self->meta->i18n_translation_rel_name() ) {
303 my $i18n = {};
305 while ( my ( $key, $val ) = each %params ) {
306 $i18n->{ $key } = delete $params{ $key } unless $self->can( $key );
309 if ( %$i18n ) {
310 $params{ $rel_name } ||= {};
311 $params{ $rel_name } = { %$i18n, %{ $params{ $rel_name } } };
315 $self->SUPER::init( %params );
318 =head2 save
320 CREATE
322 Data that is static is added to static table, then for each language
323 translatable data is added to translations table with a flag (istran) that there
324 is no translation.
326 UPDATE
328 If updating original language data update it and then synchronize with all
329 translations that are not translations (the data is the same, istran flag is 0)
331 If updating translation set istran to 1 and update all columns as usual.
333 =cut
335 sub save {
336 my $self = shift;
337 my %params = @_;
339 if (my $rel_name = $self->meta->i18n_translation_rel_name()) {
340 my $i18n_save = 0;
341 #if ( !$self->has_loaded_related( $rel_name ) && $self->{ _i18n } ) {
342 if ( $self->{ _i18n } ) {
343 $i18n_save = 1;
346 $self->i18n->save() if $i18n_save && !$params{noi18n};
347 #$self->i18n->save();
350 $self->SUPER::save(@_);
353 sub insert {
354 my $self = shift;
356 if ( my $rel_name = $self->meta->i18n_translation_rel_name() ) {
357 die 'no languages provided' unless $self->i18n_languages;
359 if ( $self->$rel_name ) {
360 my $i18n = shift @{ $self->$rel_name };
362 my $i18n_lang_column = $i18n->_i18n_lang_column;
363 my $i18n_istran_column = $i18n->_i18n_istran_column;
365 my $add_method = "add_$rel_name";
366 foreach my $lang ( @{ $self->i18n_languages } ) {
367 $i18n->$i18n_lang_column( $lang );
368 $i18n->$i18n_istran_column( 0 );
369 $self->$add_method(
370 { map { $_ => $i18n->$_ } @{ $i18n->meta->column_names } }
373 } else {
374 my ( $rel ) = grep { $_->name eq $rel_name } $self->meta->relationships;
376 my ( $i18n_lang ) = grep { $_->type eq 'i18n_language' }
377 $rel->foreign_class->meta->columns;
378 my $i18n_lang_column = $i18n_lang->name;
380 my ( $i18n_istran ) = grep { $_->type eq 'i18n_is_translation' }
381 $rel->foreign_class->meta->columns;
382 my $i18n_istran_column = $i18n_istran->name;
384 $self->$rel_name(
385 map {
386 { $i18n_lang_column => $_, $i18n_istran_column => 0 }
387 } $self->i18n_languages
392 $self->SUPER::insert( @_ );
395 sub update {
396 my $self = shift;
398 if ( $self->meta->i18n_static_rel_name() ) {
399 my $parent = $self->_i18n_parent;
401 my $orig_lang_column = $parent->_i18n_lang_column;
402 my $i18n_lang_column = $self->_i18n_lang_column;
403 my $i18n_istran_column = $self->_i18n_istran_column;
405 if ( $parent->$orig_lang_column eq $self->$i18n_lang_column ) {
406 foreach my $i18n ( $parent->not_translated_i18n() ) {
407 $i18n->_i18n_sync_with( $self );
409 } else {
410 $self->$i18n_istran_column( 1 );
414 $self->SUPER::update( @_ );
417 =head2 load
419 When you want to load default language ($ENV{RDBO_I18N_LANG} or original) just load as you
420 always do:
422 $u = User->new( id => 1 );
423 $u->load();
425 When you want to load en translation:
427 $u = User->new( id => 1 );
428 $u->load( i18n => 'en' );
430 =head2 i18n PARAM
432 Returns preloaded i18n object or, if the last was not found, preloads it taking the
433 default language or language that is provided as a parameter.
435 $u = User->new( id => 1 );
436 # let's assume that the original language is English ('en').
437 $u->load();
439 $u->i18n->title; # title is in English
440 $u->i18n('de')->title; # title is in German
441 $u->i18n('en')->title; # title is back in English
443 =cut
445 sub i18n {
446 my ( $self, $i18n ) = @_;
448 my $rel_name = $self->meta->i18n_translation_rel_name();
450 return unless $rel_name;
452 return $self->{ _i18n } if !$i18n && $self->{ _i18n };
454 if ( !$i18n && $self->has_loaded_related( $rel_name ) ) {
455 $self->{ _i18n } = $self->$rel_name->[ 0 ];
456 } else {
457 $self->_load_i18n( i18n => $i18n );
460 return $self->{ _i18n };
463 =head2 i18n_available_translations
465 Returns array reference of another available translations.
467 =cut
469 sub i18n_available_translations {
470 my $self = shift;
472 my $rel_name = $self->meta->i18n_translation_rel_name();
473 return unless $rel_name;
475 my $method = "find_$rel_name";
477 unless ( $self->i18n_is_loaded() ) {
478 $self->error( "first do i18n()" );
479 $self->meta->handle_error( $self );
480 return;
483 my $orig_lang_column = $self->_i18n_lang_column;
484 my $i18n_lang_column = $self->i18n->_i18n_lang_column;
485 my $i18n_istran_column = $self->i18n->_i18n_istran_column;
487 my $orig_lang = $self->$orig_lang_column;
488 my $lang = $self->i18n->$i18n_lang_column;
490 my $subquery;
491 if ( $self->i18n_is_original_loaded() ) {
492 $subquery = [ $i18n_istran_column => 1, ];
493 } else {
494 $subquery = [
495 or => [
496 $i18n_istran_column => 1,
497 $i18n_lang_column => $orig_lang
502 my $i18n = $self->$method(
503 query => [
504 $i18n_lang_column => { ne => $lang },
505 @$subquery
507 select => $i18n_lang_column
510 return [ map { $_->$i18n_lang_column } @$i18n ];
513 =head2 i18n_is_original_loaded
515 Returns if loaded translation is original.
517 =cut
519 sub i18n_is_original_loaded {
520 my $self = shift;
522 my $orig_lang_column = $self->_i18n_lang_column;
523 my $i18n_lang_column = $self->i18n->_i18n_lang_column;
524 my $i18n_istran_column = $self->i18n->_i18n_istran_column;
526 return $self->$orig_lang_column eq $self->i18n->$i18n_lang_column
527 || $self->i18n->$i18n_istran_column == 0 ? 1 : 0;
530 =head2 not_translated_i18n
532 Return array reference of languages that have no translation.
534 =head2 delete_i18n
536 Delete currently loaded translation and loads original.
538 =cut
540 sub delete_i18n {
541 my $self = shift;
543 return if $self->i18n_is_original_loaded();
545 my $orig_lang_column = $self->_i18n_lang_column;
546 my $i18n_lang_column = $self->i18n->_i18n_lang_column;
547 my $i18n_istran_column = $self->i18n->_i18n_istran_column;
549 return unless $self->i18n->$i18n_istran_column;
551 my $translation_rel_name = $self->meta->i18n_translation_rel_name();
552 my $method = "find_$translation_rel_name";
554 my $original_i18n = $self->$method(
555 query => [
556 $i18n_istran_column => 0,
557 $i18n_lang_column => $self->$orig_lang_column
559 )->[ 0 ];
561 $self->i18n->$i18n_istran_column( 0 );
562 $self->i18n->_i18n_sync_with( $original_i18n );
565 =head2 Rose::DBx::Object::I18N::Manager
567 On selection there is only one join, no need to do any logic selection, because
568 we have all data ready for selection at the right place. If there was no
569 translation, anyway data will be there, it will be original, because no
570 translation was updated.
572 get_objects method is overloaded, so you don't have to provide query with the
573 language selection and table to join, just use is transparently:
575 User::Manager->get_objects( i18n => 'en' );
577 =cut
579 sub _load_i18n {
580 my $self = shift;
581 my %args = @_;
583 my $language = $args{ i18n } || $self->i18n_language();
585 my $rel_name = $self->meta->i18n_translation_rel_name();
587 my $meta = $self->meta;
589 my ( $rel ) = grep { $_->name eq $rel_name } $self->meta->relationships;
590 my ( $i18n_lang ) =
591 grep { $_->type eq 'i18n_language' } $rel->foreign_class->meta->columns;
592 my $i18n_lang_column = $i18n_lang->name;
594 my $method = "find_$rel_name";
595 my $i18n = $self->$method( [ $i18n_lang_column => $language ] );
597 my $loaded_ok = $i18n ? $i18n->[ 0 ] ? 1 : 0 : 0;
599 unless ( $loaded_ok ) {
600 my $speculative =
601 exists $args{ 'speculative' }
602 ? $args{ 'speculative' }
603 : $meta->default_load_speculative;
605 unless ( $speculative ) {
606 $self->error( "load_i18n() - can't find $language translation" );
607 $meta->handle_error( $self );
610 return 0;
613 $self->{ _i18n } = $i18n->[ 0 ];
615 return 1;
618 sub not_translated_i18n {
619 my $self = shift;
621 my $translation_rel_name = $self->meta->i18n_translation_rel_name();
622 my $method = "find_$translation_rel_name";
624 my $orig_lang_column = $self->_i18n_lang_column;
625 my $i18n_lang_column = $self->i18n->_i18n_lang_column;
626 my $i18n_istran_column = $self->i18n->_i18n_istran_column;
628 my @i18n = $self->$method(
629 query => [
630 $i18n_istran_column => 0,
631 $i18n_lang_column => { ne => $self->$orig_lang_column }
635 return wantarray ? @i18n : \@i18n;
638 sub _i18n_parent {
639 my $self = shift;
641 my $rel_name = $self->meta->i18n_static_rel_name();
642 return $self->$rel_name;
645 sub _i18n_sync_with {
646 my $self = shift;
647 my ( $from ) = @_;
649 my $i18n_lang_column = $self->_i18n_lang_column;
650 my $i18n_istran_column = $self->_i18n_istran_column;
652 my ( $pk ) = $self->meta->primary_key_column_names;
654 my @columns =
655 grep { $_ !~ m/(?:$pk|$i18n_istran_column|$i18n_lang_column)/ }
656 $self->meta->column_names();
658 my @debug;
659 foreach my $column ( @columns ) {
660 my $old = $self->$column;
661 $self->$column( $from->$column );
663 $self->SUPER::update();
666 sub i18n_is_loaded
668 my $self = shift;
670 my $rel_name = $self->meta->i18n_translation_rel_name();
672 return $self->has_loaded_related( $rel_name ) || $self->{ _i18n } ? 1 : 0;
675 sub _i18n_istran_column {
676 my $self = shift;
678 my ( $column ) =
679 grep { $_->type eq 'i18n_is_translation' } @{ $self->meta->columns };
681 return $column->name;
684 sub _i18n_lang_column {
685 my $self = shift;
687 my ( $column ) =
688 grep { $_->type eq 'i18n_language' } @{ $self->meta->columns };
690 return $column->name;
693 use constant LAZY_LOADED_KEY =>
694 Rose::DB::Object::Util::lazy_column_values_loaded_key();
696 sub load
698 my($self) = $_[0]; # XXX: Must maintain alias to actual "self" object arg
700 my %args = (self => @_); # faster than @_[1 .. $#_];
702 $self->SUPER::load( %args ) if $self->meta->i18n_static_rel_name();
704 my $db = $self->db or return 0;
705 my $dbh = $self->dbh or return 0;
707 my $meta = $self->meta;
709 my $prepare_cached =
710 exists $args{'prepare_cached'} ? $args{'prepare_cached'} :
711 $meta->dbi_prepare_cached;
713 local $self->{STATE_SAVING()} = 1;
715 my(@key_columns, @key_methods, @key_values);
717 my $null_key = 0;
718 my $found_key = 0;
720 if ( my $i18n = (delete $args{ i18n }) ) {
721 my $rel_name = $self->meta->i18n_translation_rel_name();
722 my $new_args = merge {
723 query => ["$rel_name.lang" => $i18n],
724 with => [ $rel_name ]
725 }, \%args;
727 %args = %$new_args;
730 if(my $key = delete $args{'use_key'})
732 my @uk = grep { $_->name eq $key } $meta->unique_keys;
734 if(@uk == 1)
736 my $defined = 0;
737 @key_columns = $uk[0]->column_names;
738 @key_methods = map { $meta->column_accessor_method_name($_) } @key_columns;
739 @key_values = map { $defined++ if(defined $_); $_ }
740 map { $self->$_() } @key_methods;
742 unless($defined)
744 $self->error("Could not load() based on key '$key' - column(s) have undefined values");
745 $meta->handle_error($self);
746 return undef;
749 if(@key_values != $defined)
751 $null_key = 1;
754 else { Carp::croak "No unique key named '$key' is defined in ", ref($self) }
756 else
758 @key_columns = $meta->primary_key_column_names;
759 @key_methods = $meta->primary_key_column_accessor_names;
760 @key_values = grep { defined } map { $self->$_() } @key_methods;
762 unless(@key_values == @key_columns)
764 my $alt_columns;
766 # Prefer unique keys where we have defined values for all
767 # key columns, but fall back to the first unique key found
768 # where we have at least one defined value.
769 foreach my $cols ($meta->unique_keys_column_names)
771 my $defined = 0;
772 @key_columns = @$cols;
773 @key_methods = map { $meta->column_accessor_method_name($_) } @key_columns;
774 @key_values = map { $defined++ if(defined $_); $_ }
775 map { $self->$_() } @key_methods;
777 if($defined == @key_columns)
779 $found_key = 1;
780 last;
783 $alt_columns ||= $cols if($defined);
786 if(!$found_key && $alt_columns)
788 @key_columns = @$alt_columns;
789 @key_methods = map { $meta->column_accessor_method_name($_) } @key_columns;
790 @key_values = map { $self->$_() } @key_methods;
791 $null_key = 1;
792 $found_key = 1;
795 unless($found_key)
797 @key_columns = $meta->primary_key_column_names;
799 my $e =
800 Rose::DB::Object::Exception->new(
801 message => "Cannot load " . ref($self) . " without a primary key (" .
802 join(', ', @key_columns) . ') with ' .
803 (@key_columns > 1 ? 'non-null values in all columns' :
804 'a non-null value') .
805 ' or another unique key with at least one non-null value.',
806 code => EXCEPTION_CODE_NO_KEY);
808 $self->error($e);
809 $meta->handle_error($self);
810 return 0;
815 my $has_lazy_columns = $args{'nonlazy'} ? 0 : $meta->has_lazy_columns;
816 my $column_names;
818 if($has_lazy_columns)
820 $column_names = $meta->nonlazy_column_names;
821 $self->{LAZY_LOADED_KEY()} = {};
823 else
825 $column_names = $meta->column_names;
829 # Handle sub-object load in separate code path
832 if(my $with = $args{'with'})
834 my $mgr_class = $args{'manager_class'} || 'Rose::DB::Object::Manager';
835 my %query;
837 @query{map { "t1.$_" } @key_columns} = @key_values;
840 $args{query} ||= [];
842 %query = ( @{ $args{query} }, %query );
844 #use Data::Dumper;
845 #print Dumper $args{query};
846 #print Dumper \%query;
848 my $objects;
850 eval
852 $objects =
853 $mgr_class->get_objects(object_class => ref $self,
854 db => $db,
855 query => [ %query ],
856 with_objects => $with,
857 multi_many_ok => 1,
858 nonlazy => $args{'nonlazy'},
859 inject_results => $args{'inject_results'},
860 (exists $args{'prepare_cached'} ?
861 (prepare_cached => $args{'prepare_cached'}) :
862 ()))
863 or Carp::confess $mgr_class->error;
865 if(@$objects > 1)
867 die "Found ", @$objects, " objects instead of one";
871 if($@)
873 $self->error("load(with => ...) - $@");
874 $meta->handle_error($self);
875 return undef;
878 if(@$objects > 0)
880 # Sneaky init by object replacement
881 $self = $_[0] = $objects->[0];
883 # Init by copying attributes (broken; need to do fks and relationships too)
884 #my $methods = $meta->column_mutator_method_names;
885 #my $object = $objects->[0];
887 #local $self->{STATE_LOADING()} = 1;
888 #local $object->{STATE_SAVING()} = 1;
890 #foreach my $method (@$methods)
892 # $self->$method($object->$method());
895 else
897 no warnings;
898 $self->error("No such " . ref($self) . ' where ' .
899 join(', ', @key_columns) . ' = ' . join(', ', @key_values));
900 $self->{'not_found'} = 1;
902 $self->{STATE_IN_DB()} = 0;
904 my $speculative =
905 exists $args{'speculative'} ? $args{'speculative'} :
906 $meta->default_load_speculative;
908 unless($speculative)
910 $meta->handle_error($self);
913 return 0;
916 $self->{STATE_IN_DB()} = 1;
917 $self->{LOADED_FROM_DRIVER()} = $db->{'driver'};
918 $self->{MODIFIED_COLUMNS()} = {};
919 return $self || 1;
923 # Handle normal load
926 my $loaded_ok;
928 $self->{'not_found'} = 0;
930 eval
932 local $self->{STATE_LOADING()} = 1;
933 local $dbh->{'RaiseError'} = 1;
935 my($sql, $sth);
937 if($null_key)
939 if($has_lazy_columns)
941 $sql = $meta->load_sql_with_null_key(\@key_columns, \@key_values, $db);
943 else
945 $sql = $meta->load_all_sql_with_null_key(\@key_columns, \@key_values, $db);
948 else
950 if($has_lazy_columns)
952 $sql = $meta->load_sql(\@key_columns, $db);
954 else
956 $sql = $meta->load_all_sql(\@key_columns, $db);
960 # $meta->prepare_select_options (defunct)
961 $sth = $prepare_cached ? $dbh->prepare_cached($sql, undef, 3) :
962 $dbh->prepare($sql);
964 $Debug && warn "$sql - bind params: ", join(', ', grep { defined } @key_values), "\n";
965 $sth->execute(grep { defined } @key_values);
967 my %row;
969 $sth->bind_columns(undef, \@row{@$column_names});
971 $loaded_ok = defined $sth->fetch;
973 # The load() query shouldn't find more than one row anyway,
974 # but DBD::SQLite demands this :-/
975 $sth->finish;
977 if($loaded_ok)
979 my $methods = $meta->column_mutator_method_names_hash;
981 # Empty existing object?
982 #%$self = (db => $self->db, meta => $meta, STATE_LOADING() => 1);
984 foreach my $name (@$column_names)
986 my $method = $methods->{$name};
987 $self->$method($row{$name});
990 # Sneaky init by object replacement
991 #my $object = (ref $self)->new(db => $self->db);
993 #foreach my $name (@$column_names)
995 # my $method = $methods->{$name};
996 # $object->$method($row{$name});
999 #$self = $_[0] = $object;
1001 else
1003 no warnings;
1004 $self->error("No such " . ref($self) . ' where ' .
1005 join(', ', @key_columns) . ' = ' . join(', ', @key_values));
1006 $self->{'not_found'} = 1;
1007 $self->{STATE_IN_DB()} = 0;
1011 if($@)
1013 $self->error("load() - $@");
1014 $meta->handle_error($self);
1015 return undef;
1018 unless($loaded_ok)
1020 my $speculative =
1021 exists $args{'speculative'} ? $args{'speculative'} :
1022 $meta->default_load_speculative;
1024 unless($speculative)
1026 $meta->handle_error($self);
1029 return 0;
1032 $self->{STATE_IN_DB()} = 1;
1033 $self->{LOADED_FROM_DRIVER()} = $db->{'driver'};
1034 $self->{MODIFIED_COLUMNS()} = {};
1035 return $self || 1;
1038 =head1 COPYRIGHT & LICENSE
1040 Copyright 2008 Viacheslav Tikhanovskii, all rights reserved.
1042 This program is free software; you can redistribute it and/or modify it
1043 under the same terms as Perl itself.
1045 =cut