Documentation.
[blog.pm-common-perl-mods.git] / Rose-DBx-Object-I18N / lib / Rose / DBx / Object / I18N.pm
blob3012230f1a1a66256b32e359d13169371a896943
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.01;
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 ) =
377 grep { $_->type eq 'i18n_language' } $rel->foreign_class->meta->columns;
378 my $i18n_lang_column = $i18n_lang->name;
380 my ( $i18n_istran ) =
381 grep { $_->type eq 'i18n_is_translation' } $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{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 Return 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
536 Deletes record with translations.
538 =head2 delete_i18n
540 Delete currently loaded translation and loads original.
542 =cut
544 sub delete_i18n {
545 my $self = shift;
547 return if $self->i18n_is_original_loaded();
549 my $orig_lang_column = $self->_i18n_lang_column;
550 my $i18n_lang_column = $self->i18n->_i18n_lang_column;
551 my $i18n_istran_column = $self->i18n->_i18n_istran_column;
553 return unless $self->i18n->$i18n_istran_column;
555 my $translation_rel_name = $self->meta->i18n_translation_rel_name();
556 my $method = "find_$translation_rel_name";
558 my $original_i18n = $self->$method(
559 query => [
560 $i18n_istran_column => 0,
561 $i18n_lang_column => $self->$orig_lang_column
563 )->[ 0 ];
565 $self->i18n->$i18n_istran_column( 0 );
566 $self->i18n->_i18n_sync_with( $original_i18n );
569 =head2 Rose::DBx::Object::I18N::Manager
571 On selection there is only one join, no need to do any logic selection, because
572 we have all data ready for selection at the right place. If there was no
573 translation, anyway data will be there, it will be original, because no
574 translation was updated.
576 get_objects method is overloaded, so you don't have to provide query with the
577 language selection and table to join, just use is transparently:
579 User::Manager->get_objects( i18n => 'en' );
581 =cut
583 sub _load_i18n {
584 my $self = shift;
585 my %args = @_;
587 my $language = $args{ i18n } || $self->i18n_language();
589 my $rel_name = $self->meta->i18n_translation_rel_name();
591 my $meta = $self->meta;
593 my ( $rel ) = grep { $_->name eq $rel_name } $self->meta->relationships;
594 my ( $i18n_lang ) =
595 grep { $_->type eq 'i18n_language' } $rel->foreign_class->meta->columns;
596 my $i18n_lang_column = $i18n_lang->name;
598 my $method = "find_$rel_name";
599 my $i18n = $self->$method( [ $i18n_lang_column => $language ] );
601 my $loaded_ok = $i18n ? $i18n->[ 0 ] ? 1 : 0 : 0;
603 unless ( $loaded_ok ) {
604 my $speculative =
605 exists $args{ 'speculative' }
606 ? $args{ 'speculative' }
607 : $meta->default_load_speculative;
609 unless ( $speculative ) {
610 $self->error( "load_i18n() - can't find $language translation" );
611 $meta->handle_error( $self );
614 return 0;
617 $self->{ _i18n } = $i18n->[ 0 ];
619 return 1;
622 sub not_translated_i18n {
623 my $self = shift;
625 my $translation_rel_name = $self->meta->i18n_translation_rel_name();
626 my $method = "find_$translation_rel_name";
628 my $orig_lang_column = $self->_i18n_lang_column;
629 my $i18n_lang_column = $self->i18n->_i18n_lang_column;
630 my $i18n_istran_column = $self->i18n->_i18n_istran_column;
632 my @i18n = $self->$method(
633 query => [
634 $i18n_istran_column => 0,
635 $i18n_lang_column => { ne => $self->$orig_lang_column }
639 return wantarray ? @i18n : \@i18n;
642 sub _i18n_parent {
643 my $self = shift;
645 my $rel_name = $self->meta->i18n_static_rel_name();
646 return $self->$rel_name;
649 sub _i18n_sync_with {
650 my $self = shift;
651 my ( $from ) = @_;
653 my $i18n_lang_column = $self->_i18n_lang_column;
654 my $i18n_istran_column = $self->_i18n_istran_column;
656 my ( $pk ) = $self->meta->primary_key_column_names;
658 my @columns =
659 grep { $_ !~ m/(?:$pk|$i18n_istran_column|$i18n_lang_column)/ }
660 $self->meta->column_names();
662 my @debug;
663 foreach my $column ( @columns ) {
664 my $old = $self->$column;
665 $self->$column( $from->$column );
667 $self->SUPER::update();
670 sub i18n_is_loaded
672 my $self = shift;
674 my $rel_name = $self->meta->i18n_translation_rel_name();
676 return $self->has_loaded_related( $rel_name ) || $self->{ _i18n } ? 1 : 0;
679 sub _i18n_istran_column {
680 my $self = shift;
682 my ( $column ) =
683 grep { $_->type eq 'i18n_is_translation' } @{ $self->meta->columns };
685 return $column->name;
688 sub _i18n_lang_column {
689 my $self = shift;
691 my ( $column ) =
692 grep { $_->type eq 'i18n_language' } @{ $self->meta->columns };
694 return $column->name;
697 use constant LAZY_LOADED_KEY =>
698 Rose::DB::Object::Util::lazy_column_values_loaded_key();
700 sub load
702 my($self) = $_[0]; # XXX: Must maintain alias to actual "self" object arg
704 my %args = (self => @_); # faster than @_[1 .. $#_];
706 $self->SUPER::load( %args ) if $self->meta->i18n_static_rel_name();
708 my $db = $self->db or return 0;
709 my $dbh = $self->dbh or return 0;
711 my $meta = $self->meta;
713 my $prepare_cached =
714 exists $args{'prepare_cached'} ? $args{'prepare_cached'} :
715 $meta->dbi_prepare_cached;
717 local $self->{STATE_SAVING()} = 1;
719 my(@key_columns, @key_methods, @key_values);
721 my $null_key = 0;
722 my $found_key = 0;
724 if ( my $i18n = (delete $args{ i18n }) ) {
725 my $rel_name = $self->meta->i18n_translation_rel_name();
726 my $new_args = merge {
727 query => ["$rel_name.lang" => $i18n],
728 with => [ $rel_name ]
729 }, \%args;
731 %args = %$new_args;
734 if(my $key = delete $args{'use_key'})
736 my @uk = grep { $_->name eq $key } $meta->unique_keys;
738 if(@uk == 1)
740 my $defined = 0;
741 @key_columns = $uk[0]->column_names;
742 @key_methods = map { $meta->column_accessor_method_name($_) } @key_columns;
743 @key_values = map { $defined++ if(defined $_); $_ }
744 map { $self->$_() } @key_methods;
746 unless($defined)
748 $self->error("Could not load() based on key '$key' - column(s) have undefined values");
749 $meta->handle_error($self);
750 return undef;
753 if(@key_values != $defined)
755 $null_key = 1;
758 else { Carp::croak "No unique key named '$key' is defined in ", ref($self) }
760 else
762 @key_columns = $meta->primary_key_column_names;
763 @key_methods = $meta->primary_key_column_accessor_names;
764 @key_values = grep { defined } map { $self->$_() } @key_methods;
766 unless(@key_values == @key_columns)
768 my $alt_columns;
770 # Prefer unique keys where we have defined values for all
771 # key columns, but fall back to the first unique key found
772 # where we have at least one defined value.
773 foreach my $cols ($meta->unique_keys_column_names)
775 my $defined = 0;
776 @key_columns = @$cols;
777 @key_methods = map { $meta->column_accessor_method_name($_) } @key_columns;
778 @key_values = map { $defined++ if(defined $_); $_ }
779 map { $self->$_() } @key_methods;
781 if($defined == @key_columns)
783 $found_key = 1;
784 last;
787 $alt_columns ||= $cols if($defined);
790 if(!$found_key && $alt_columns)
792 @key_columns = @$alt_columns;
793 @key_methods = map { $meta->column_accessor_method_name($_) } @key_columns;
794 @key_values = map { $self->$_() } @key_methods;
795 $null_key = 1;
796 $found_key = 1;
799 unless($found_key)
801 @key_columns = $meta->primary_key_column_names;
803 my $e =
804 Rose::DB::Object::Exception->new(
805 message => "Cannot load " . ref($self) . " without a primary key (" .
806 join(', ', @key_columns) . ') with ' .
807 (@key_columns > 1 ? 'non-null values in all columns' :
808 'a non-null value') .
809 ' or another unique key with at least one non-null value.',
810 code => EXCEPTION_CODE_NO_KEY);
812 $self->error($e);
813 $meta->handle_error($self);
814 return 0;
819 my $has_lazy_columns = $args{'nonlazy'} ? 0 : $meta->has_lazy_columns;
820 my $column_names;
822 if($has_lazy_columns)
824 $column_names = $meta->nonlazy_column_names;
825 $self->{LAZY_LOADED_KEY()} = {};
827 else
829 $column_names = $meta->column_names;
833 # Handle sub-object load in separate code path
836 if(my $with = $args{'with'})
838 my $mgr_class = $args{'manager_class'} || 'Rose::DB::Object::Manager';
839 my %query;
841 @query{map { "t1.$_" } @key_columns} = @key_values;
844 $args{query} ||= [];
846 %query = ( @{ $args{query} }, %query );
848 #use Data::Dumper;
849 #print Dumper $args{query};
850 #print Dumper \%query;
852 my $objects;
854 eval
856 $objects =
857 $mgr_class->get_objects(object_class => ref $self,
858 db => $db,
859 query => [ %query ],
860 with_objects => $with,
861 multi_many_ok => 1,
862 nonlazy => $args{'nonlazy'},
863 inject_results => $args{'inject_results'},
864 (exists $args{'prepare_cached'} ?
865 (prepare_cached => $args{'prepare_cached'}) :
866 ()))
867 or Carp::confess $mgr_class->error;
869 if(@$objects > 1)
871 die "Found ", @$objects, " objects instead of one";
875 if($@)
877 $self->error("load(with => ...) - $@");
878 $meta->handle_error($self);
879 return undef;
882 if(@$objects > 0)
884 # Sneaky init by object replacement
885 $self = $_[0] = $objects->[0];
887 # Init by copying attributes (broken; need to do fks and relationships too)
888 #my $methods = $meta->column_mutator_method_names;
889 #my $object = $objects->[0];
891 #local $self->{STATE_LOADING()} = 1;
892 #local $object->{STATE_SAVING()} = 1;
894 #foreach my $method (@$methods)
896 # $self->$method($object->$method());
899 else
901 no warnings;
902 $self->error("No such " . ref($self) . ' where ' .
903 join(', ', @key_columns) . ' = ' . join(', ', @key_values));
904 $self->{'not_found'} = 1;
906 $self->{STATE_IN_DB()} = 0;
908 my $speculative =
909 exists $args{'speculative'} ? $args{'speculative'} :
910 $meta->default_load_speculative;
912 unless($speculative)
914 $meta->handle_error($self);
917 return 0;
920 $self->{STATE_IN_DB()} = 1;
921 $self->{LOADED_FROM_DRIVER()} = $db->{'driver'};
922 $self->{MODIFIED_COLUMNS()} = {};
923 return $self || 1;
927 # Handle normal load
930 my $loaded_ok;
932 $self->{'not_found'} = 0;
934 eval
936 local $self->{STATE_LOADING()} = 1;
937 local $dbh->{'RaiseError'} = 1;
939 my($sql, $sth);
941 if($null_key)
943 if($has_lazy_columns)
945 $sql = $meta->load_sql_with_null_key(\@key_columns, \@key_values, $db);
947 else
949 $sql = $meta->load_all_sql_with_null_key(\@key_columns, \@key_values, $db);
952 else
954 if($has_lazy_columns)
956 $sql = $meta->load_sql(\@key_columns, $db);
958 else
960 $sql = $meta->load_all_sql(\@key_columns, $db);
964 # $meta->prepare_select_options (defunct)
965 $sth = $prepare_cached ? $dbh->prepare_cached($sql, undef, 3) :
966 $dbh->prepare($sql);
968 $Debug && warn "$sql - bind params: ", join(', ', grep { defined } @key_values), "\n";
969 $sth->execute(grep { defined } @key_values);
971 my %row;
973 $sth->bind_columns(undef, \@row{@$column_names});
975 $loaded_ok = defined $sth->fetch;
977 # The load() query shouldn't find more than one row anyway,
978 # but DBD::SQLite demands this :-/
979 $sth->finish;
981 if($loaded_ok)
983 my $methods = $meta->column_mutator_method_names_hash;
985 # Empty existing object?
986 #%$self = (db => $self->db, meta => $meta, STATE_LOADING() => 1);
988 foreach my $name (@$column_names)
990 my $method = $methods->{$name};
991 $self->$method($row{$name});
994 # Sneaky init by object replacement
995 #my $object = (ref $self)->new(db => $self->db);
997 #foreach my $name (@$column_names)
999 # my $method = $methods->{$name};
1000 # $object->$method($row{$name});
1003 #$self = $_[0] = $object;
1005 else
1007 no warnings;
1008 $self->error("No such " . ref($self) . ' where ' .
1009 join(', ', @key_columns) . ' = ' . join(', ', @key_values));
1010 $self->{'not_found'} = 1;
1011 $self->{STATE_IN_DB()} = 0;
1015 if($@)
1017 $self->error("load() - $@");
1018 $meta->handle_error($self);
1019 return undef;
1022 unless($loaded_ok)
1024 my $speculative =
1025 exists $args{'speculative'} ? $args{'speculative'} :
1026 $meta->default_load_speculative;
1028 unless($speculative)
1030 $meta->handle_error($self);
1033 return 0;
1036 $self->{STATE_IN_DB()} = 1;
1037 $self->{LOADED_FROM_DRIVER()} = $db->{'driver'};
1038 $self->{MODIFIED_COLUMNS()} = {};
1039 return $self || 1;
1042 =head1 COPYRIGHT & LICENSE
1044 Copyright 2008 Viacheslav Tikhanovskii, all rights reserved.
1046 This program is free software; you can redistribute it and/or modify it
1047 under the same terms as Perl itself.
1049 =cut