[Dbix-class] New module for DBIx::Class (Store many columns in one)

Oleg Pronin syber.rus at gmail.com
Sun Feb 18 20:05:38 GMT 2007


Skipped content of type multipart/alternative-------------- next part -----=
---------
package DBIx::Class::FrozenColumns;
use base qw/DBIx::Class/;

use strict;
use warnings;

__PACKAGE__->mk_classdata('_frozen_columns' =3D> {});
__PACKAGE__->mk_classdata('_dirty_frozen_columns');

=3Dhead1 NAME

DBIx::Class::FrozenColumns - Store virtual columns inside another column.

=3Dhead1 SYNOPSIS

    package Artist;
    __PACKAGE__->load_components(qw/FrozenColumns Core/);
    __PACKAGE__->add_columns(qw/name description frozen/);
    __PACKAGE__->add_frozen_columns(
        frozen =3D> qw/biography url img50x50 img100x100/
    );
    =

    $artist->url('http://cpan.org');
    $artist->get_column('url');
    $artist->get_dirty_columns; # 'url' and 'frozen' are dirty
    $artist->update; #updates column 'frozen' (using Storable::freeze)
    =

    $artistRS->create({
        name     =3D> 'theodor bastard',
        img50x50 =3D> '50x50.gif',
    }); #that's ok. 'img50x50' will be stored in 'frozen'
    =

    my @artists =3D $artistRS->search({
        name =3D> '.....',
        url  =3D> 'http://cpan.org',
    }); # Error! no such column 'url'
    =

    package Artist;
    __PACKAGE__->add_frozen_columns(
        biography =3D> qw/childhood adolescence youth present/,
    );
    =

    $artist->childhood('bla-bla-bla');
    $artist->update; #Updates column 'frozen'.

=3Dhead1 DESCRIPTION

This module allows you to store multiple columns in one. This is useful when
you want to store dynamic number of columns in database or you just don't k=
now
what columns will be stored there. Or when you can't (or don't want) to alt=
er
your tables in database.

Module allows you to transparently use this columns as if they were normal
columns in your table. With one obvious restriction: you cannot search rows=
 in a
table and therefore you cannot add relationships using these columns (searc=
h is
needed to build reverse relationship).

Module handles its own dirty column management and will not update the pare=
nt
field unless any columns is changed.

Note: The component needs to be loaded before Core.

Also note that frozen column IS NOT a real column of your result class.
This impose some restrictions on use of this columns such as searching, add=
ing
relationships, has_column, get_columns, etc.
See L</EXTENDED METHODS> for the list of method that will work with frozen
columns (as will methods that use it).

Module unpacks frozen columns only once when you first accessing it and pac=
ks
when you call update.

You can also create frozen columns in another frozen column any level deep.
The only restriction is that they all use the same storing mechanism.

=3Dhead1 METHODS

=3Dhead2 add_frozen_columns

    __PACKAGE__->add_frozen_columns ($data_column, @columns)
    __PACKAGE__->add_frozen_columns ($hashref)

Adds frozen @columns to your result source class. These columns will be sto=
red in
$data_column using Storable freeze/thaw algorithm.
If $hashref is specified instead, then below params is expected in it:
    data_column - same as $data_column
    columns     - same as @columns
    type        - class with custom mechanism of storing/restoring frozen c=
ols
See below for more information about L</Custom frozen class>.

=3Dhead2 add_dumped_columns ($data_column, @columns)

Same as L</add_frozen_columns> but uses Data::Dumper mechanism.

=3Dcut

sub add_frozen_columns {
    my $this =3D shift;
    my ($type, $data_column, @frozen_columns);

    if(ref $_[0]) {
        my $params =3D shift;
        $type           =3D $params->{type};
        $data_column    =3D $params->{data_column};
        @frozen_columns =3D @{$params->{columns}||[]};
    }
    else {
        $type =3D 'DBIx::Class::FrozenColumns::Frozen';
        ($data_column, @frozen_columns) =3D @_;
    }

    $this->throw_exception('Cannot store frozen columns inside another froz=
en column of different type')
     if exists $this->_frozen_columns->{$data_column}
        and $this->_frozen_columns->{$data_column}{type} ne $type;
    $this->add_column($data_column)
     unless $this->has_column($data_column) or $this->_frozen_columns->{$da=
ta_column};
    my %frozen_columns =3D %{$this->_frozen_columns};
    foreach my $f_column (@frozen_columns) {
        $this->throw_exception('Cannot override existing column with frozen=
 one')
         if $this->has_column($f_column) or $this->_frozen_columns->{$f_col=
umn};
        $frozen_columns{$f_column} =3D {
            column =3D> $data_column,
            type   =3D> $type,
        };
        no strict 'refs';
        *{"${this}::$f_column"} =3D sub {
            my $self =3D shift;
            return $self->get_column($f_column) unless @_;
            $self->set_column($f_column, shift);
        };
    }
    $this->_frozen_columns(\%frozen_columns);
}

sub add_dumped_columns {
    shift->add_frozen_columns({
        type        =3D> 'DBIx::Class::FrozenColumns::Dumped',
        data_column =3D> shift,
        columns     =3D> [@_],
    });
}

=3Dhead1 EXTENDED METHODS

=3Dhead2 new

Accepts initial values for frozen columns.

    $artistRS->new({img50x50 =3D> '50x50.gif'});

=3Dcut

sub new {
    my $self =3D shift;
    my ($attrs) =3D @_;
    my %fattrs;

    foreach my $attr (keys %$attrs) {
        next unless exists $self->_frozen_columns->{$attr};
        $fattrs{$attr} =3D delete $attrs->{$attr};
    }

    my $ret =3D $self->next::method(@_);
    while ( my($k,$v) =3D each %fattrs ) {
        $ret->store_column($k, $v);
    }
    return $ret;
}


=3Dhead2 get_column

=3Dcut

sub get_column {
    my ($self, $column) =3D @_;

    if (my $frozen_info =3D $self->_frozen_columns->{$column}) {
        $self->_ensure_column_unpacked( $frozen_info->{column}, $frozen_inf=
o->{type} );
        return $self->get_column( $frozen_info->{column} )->{$column};
    }

    return $self->next::method($column);
}

=3Dhead2 get_columns

Returns DBIC's get_columns with frozen columns hash.
IMPORTANT: until $row is not in storage this method will return basic get_c=
olumns
result without frozen columns. This is needed for correct work of insert me=
thod.

=3Dcut

sub get_columns {
    my $self =3D shift;
    return $self->next::method(@_) unless $self->in_storage;
    my %data =3D $self->next::method(@_);
    foreach my $f_column ( keys %{$self->_frozen_columns} ) {
        $data{$f_column} =3D $self->get_column($f_column);
    }
    return %data;
}

=3Dhead2 store_column

=3Dcut

sub store_column {
    my ($self, $column, $value) =3D @_;

    if (my $frozen_info =3D $self->_frozen_columns->{$column}) {
        $self->_ensure_column_unpacked( $frozen_info->{column}, $frozen_inf=
o->{type} );
        return $self->get_column( $frozen_info->{column} )->{$column} =3D $=
value;
    }

    return $self->next::method($column, $value);
}

=3Dhead2 set_column

=3Dcut

sub set_column {
    my ($self, $column, $value) =3D @_;

    if (my $frozen_info =3D $self->_frozen_columns->{$column}) {
        my $data_column =3D $frozen_info->{column};
        my $old =3D $self->get_column($column);
        my $ret =3D $self->store_column($column, $value);
        if(defined $old ^ defined $ret or (defined $old && $old ne $ret)) {
            $self->set_column( $data_column, $self->get_column($data_column=
) );
            my $frozen_dirty =3D $self->_dirty_frozen_columns || {};
            $frozen_dirty->{$column} =3D 1;
            $self->_dirty_frozen_columns($frozen_dirty);
        }
        return $ret;

    }

    return $self->next::method($column, $value);
}

=3Dhead2 get_dirty_columns

Returns real and frozen dirty columns.
Note that changing frozen column will result in marking at least 2 columns =
as
dirty.

=3Dcut

sub get_dirty_columns {
    my $self =3D shift;
    return $self->next::method(@_) unless $self->_dirty_frozen_columns;
    my %data =3D $self->next::method(@_);
    foreach my $f_column ( keys %{$self->_dirty_frozen_columns} ) {
        $data{$f_column} =3D $self->get_column($f_column);
    }
    return %data;
}

sub _ensure_column_unpacked {
    my ($self, $column, $type) =3D @_;
    unless ( ref (my $packed =3D $self->get_column($column)) ) {
        $self->store_column($column, $type->recover(\$packed));
    }
}

=3Dhead2 has_column_loaded

Returns true if data_column of frozen column has loaded.

=3Dcut

sub has_column_loaded {
    my ($self, $column) =3D @_;

    if (my $frozen_info =3D $self->_frozen_columns->{$column}) {
        return $self->has_column_loaded( $frozen_info->{column} );
    }

    $self->next::method($column);
}

=3Dhead2 is_column_changed

=3Dcut

sub is_column_changed {
    my ($self, $column) =3D @_;

    if ($self->_frozen_columns->{$column}) {
        my $frozen_dirty =3D $self->_dirty_frozen_columns;
        return $frozen_dirty && exists $frozen_dirty->{$column};
    }

    $self->next::method($column);
}

=3Dhead2 is_changed

=3Dcut

sub is_changed {
    my $self =3D shift;

    if(wantarray) {
        my @columns =3D $self->next::method(@_);
        my $frozen_dirty =3D $self->_dirty_frozen_columns;
        push @columns, keys %$frozen_dirty if $frozen_dirty;
        return @columns;
    }

    return 1 if $self->next::method(@_) or keys %{$self->_dirty_frozen_colu=
mns};
}

=3Dhead2 update

=3Dcut

sub update {
    my $self =3D shift;
    $self->_dirty_frozen_columns(undef);
    $self->next::method(@_);
}


=3Dhead2 insert

=3Dcut

sub insert {
    my $self =3D shift;
    $self->_dirty_frozen_columns(undef);
    $self->next::method(@_);
}

=3Dhead1 Custom frozen class

Such a class must be derived from 'DBIx::Class::FrozenColumns::Base' and is
responsible for fetching and storing frozen columns to/from a real database=
 column.
The corresponding methods are 'recover' and 'stringify'.

The best explanation is an expamle:

    package DBIx::Class::FrozenColumns::Frozen;
    use base qw/DBIx::Class::FrozenColumns::Base/;
    =

    use strict;
    use Storable qw/freeze thaw/;
    =

    sub stringify {
         freeze(shift);
    }
    =

    sub recover {
        my ($this, $dataref) =3D @_;
        my $data =3D defined $$dataref ? eval {thaw($$dataref)} || {} : {};
        bless ($data, ref $this || $this);
    }

Information actually stored in database can be used by any other programs a=
s a simple
hash (possibly containing another hashes like itself).

=3Dcut

package DBIx::Class::FrozenColumns::Base;
use strict;
use overload '.'      =3D> sub {$_[0]->stringify},
             'ne'     =3D> sub{1},
             fallback =3D> 1;

package DBIx::Class::FrozenColumns::Frozen;
use base qw/DBIx::Class::FrozenColumns::Base/;

use strict;
use Storable qw/freeze thaw/;

sub stringify {
     freeze(shift);
}

sub recover {
    my ($this, $dataref) =3D @_;
    my $data =3D defined $$dataref ? eval {thaw($$dataref)} || {} : {};
    bless ($data, ref $this || $this);
}


package DBIx::Class::FrozenColumns::Dumped;
use base qw/DBIx::Class::FrozenColumns::Base/;

use strict;
use Data::Dumper qw/Dumper/;

sub stringify {
    local $Data::Dumper::Indent =3D 0;
    Dumper(shift);
}

sub recover {
    my ($this, $dataref) =3D @_;
    our $VAR1;
    my $data =3D defined $$dataref ? eval "$$dataref" || {} : {};
    bless ($data, ref $this || $this);
}

=3Dhead1 CAVEATS

=3Dover

=3Ditem *

You cannot search rows in a table using frozen columns

=3Ditem *

You cannot add relationships using frozen columns

=3Dback

=3Dhead1 SEE ALSO

L<Storable>, L<Data::Dumper>.

=3Dhead1 AUTHOR

Oleg Pronin <syber.rus at gmail.com>

=3Dhead1 LICENSE

You may distribute this code under the same terms as Perl itself.

=3Dcut

1;


More information about the Dbix-class mailing list