package ODB::Table; =head1 NAME ODB::Table - Base class for all ODB::Tables =cut use strict; use Lingua::EN::Inflect qw( PL ); use base 'Class::Data::Inheritable'; use Want; use ODB::Table::Field; use vars qw( $AUTOLOAD ); my $debug = 1; ODB::Table->mk_classdata('table_name'); ODB::Table->mk_classdata('column_names', {}); ODB::Table->mk_classdata('has_a', {}); ODB::Table->mk_classdata('has_many', {}); ODB::Table->mk_classdata('id_column_names'); ODB::Table->mk_classdata('conn', $ODB::conn); # POSSIBLE ALTERNATIVES TO Class:Data:Inheritable: # # sub table_name { # my ($class, $val) = @_; # ${$class."::table_name"} = $val if $val; # return ${$class."::table_name"}; # } # # sub table_name { # my ($class, $val) = @_; # our $table_name; # $table_name = $val if $val; # return $table_name; # } =head1 CLASS METHODS =over =item Table::add_col($name, $local_col, $foreign_table, $foreign_col) Add a column to the class list of cols. If foreign table information is provided, also add has_a information to this table and has_many information to the foreign table. =cut sub add_col { my ($class, $name, $local_col, $foreign_table, $foreign_col) = @_; ODB::debug(2,"Add Col ($class, $name, $local_col, $foreign_table, $foreign_col)"); my $table_name = $class->table_name; if($foreign_table) { # Don't add ourselves in a circle! #return if lc($foreign_table) eq lc($table_name); if(lc($foreign_table) eq lc($table_name)) { ODB::debug(3,"Abort add_col -- $foreign_table eq $table_name"); return; } # Add to our own has_a hash $class->has_a->{$name} = [[$local_col], $foreign_table, [$foreign_col]]; # Add to the foreign table's has_many hash my $foreign_class = ODB::get_classname($foreign_table); my $my_col = lcfirst(PL($table_name)); my $me = ucfirst($table_name); $foreign_class->has_many->{$my_col} = [[$foreign_col], $me, [$local_col]]; } else { $class->column_names()->{$name} = $local_col; } } =item ODB::Table->new =item ODB::Table->new($obj) Create a new, empty, instance from this table. Unless you give it another instance to copy from. Similarly, clone creates a new instance directly from an old instance =cut sub new { my $class = shift; $class = ref($class) || $class; my $self = { @_ }; bless $self, $class; return $self; } =item ODB::Table->get_new(@id) Get a new object, given its ID =cut sub get_new { my ($class, @id) = @_; my $self = $class->new; if($self->find_by_id(@id)) { return $self; } else { return undef; } } =back =head1 INSTANCE METHODS =over =item $obj->clone Clone an object. =cut sub clone { my $self = shift; my $clone = $self->new(%$self, @_); return $clone; } sub find { my $class = shift; my $obj = $class->new; } sub sql_where_id_column_names { my ($self) = @_; my @id_column_names = @{$self->id_column_names}; @id_column_names = map { "$_ = ?" } @id_column_names; my $where_clause = "(" . join(' AND ', @id_column_names) . ")"; return $where_clause; } =item $obj->find_by_id(@id) Do a search and select the first row with the given ID =cut sub find_by_id { my ($self, @id) = @_; my $table = $self->table_name; my $where_sql = $self->sql_where_id_column_names; my $sql = qq{ SELECT * FROM $table WHERE $where_sql }; ODB::debug(2,"SQL: $sql"); my $row = $self->conn->selectrow_hashref($sql, undef, @id); return $self->{currow} = $row; } =item $obj->get_or_set_col($col) =item $obj->get_or_set_col($col, $newval) Get or set the given column. =cut sub get_or_set_col { my ($self, $col, $val) = @_; ODB::debug(2,"Get_or_set '$col' = '$val'"); if(defined $val) { $self->{currow}{$col} = $val; } return $self->{currow}{$col}; } =item get_or_set_has_a($field) =item get_or_set_has_a($field, $newval) Well, only 'get' right now. =cut sub get_or_set_has_a { my ($self, $name, $obj) = @_; ODB::debug(2,"get_has_a($self, $name, $obj)"); my ($cols, $foreign_table, $foreign_cols) = @{$self->has_a->{$name}}; my $class = ODB::get_classname($foreign_table); # Lets see if they want to set a has-a if($obj) { # They gave us a val, so we'll use it my @id = $obj->get_id; #$self->{currow}{$col} = $col_id; } else { # Technically we should be looking for $foreign_col... but for now we'll # just assume that $foreign_col is their key my @id_val = @{$self->{currow}}{@$cols}; ODB::debug(2,"lookup col id: @id_val"); $obj = $class->get_new(@id_val); } return $obj; } =item get_or_set_has_many($field) =item get_or_set_has_many($field, $newval) Well, only 'get' right now. Returns an iterator of results from the foreign table. =cut sub get_or_set_has_many { my ($self, $name, $val) = @_; my ($cols, $foreign_table, $foreign_cols) = @{$self->has_many->{$name}}; ODB::debug(2,"Has_many: @$cols, $foreign_table, @$foreign_cols"); my $class = ODB::get_classname($foreign_table); my @id_val = @{$self->{currow}}{$cols}; my $obj = $class->new(); foreach my $foreign_col (@$foreign_cols) { $obj->$foreign_col(shift @id_val); } $obj->search; return wantarray ? $obj->fetch_all : $obj; } =item $obj->get_field_obj($field, @params) Get a Field object for this field. It actually gets a _new_ one each time, right now. =cut sub get_field_obj { my ($self, $func, @params) = @_; # Maybe I'll have a persistent list here someday, but for now we'll make a # new field object every time my $field_class = ref($self) . '::Field'; my $field = $field_class->new($self, $func, @params); return $field; } =item $obj->update Save the current row to the database. If there is no ID defined then we will switch over to do an INSERT. =cut sub update { my ($self) = @_; my $table = $self->table_name; my @id = $self->get_id; return $self->insert unless @id; my (@col_names) = keys %{$self->{currow}}; @col_names = map { $_ . ' = ?' } @col_names; my $col_names = join ',', @col_names; my (@values) = values %{$self->{currow}}; my $where_sql = $self->sql_where_id_column_names; my $sql = "UPDATE $table SET $col_names WHERE $where_sql"; ODB::debug(2,"SQL: $sql [@values - @id]\n"); return $self->conn->do($sql, undef, @values, @id); } =item $obj->delete Delete the current row from the DB, if it exists. =cut sub delete { my ($self) = @_; my $table = $self->table_name; my $id_name = $self->id_name; my @id = $self->get_id; # Check to see if we even have an @id return undef unless @id; # Should we throw a warning or something? my $where_sql = $self->sql_where_id_column_names; my $sql = "DELETE FROM $table WHERE $where_sql"; ODB::debug(2,"SQL: $sql [@id]\n"); return $self->conn->do($sql, undef, @id); } sub get_id { my ($self) = @_; my @id_col = @{$self->id_column_names}; my @id; foreach my $col (@id_col) { push @id, $self->get_or_set_col($col); } return @id; } =item $obj->insert Insert this item. We assume you haven't set the unique ID... because that would be sillyness! TODO: Make this work on a multi-key table =cut sub insert { my ($self) = @_; my $table = $self->table_name; my (@col_names) = keys %{$self->{currow}}; my (@qmarks) = map { '?' } @col_names; my (@values) = values %{$self->{currow}}; my $col_names = join ',', @col_names; my $qmarks = join ',', @qmarks; my $sql = "INSERT INTO $table ($col_names) VALUES ($qmarks)"; ODB::debug(2,"SQL: $sql [@values]"); my $result = $self->conn->do($sql, undef, @values); #my $id = $self->conn->last_insert_id; my $id = $self->conn->func('last_insert_rowid'); my $id_name = $self->id_column_names->[0]; $self->$id_name($id); $self->update; return $self; } =item $obj->search Search for things which are like the current object. That is, if you have set any of this object's fields then we will look for objects with the same things. Additionally you can have magical field settings. If a field is an arrayref then we will consider it a pair of (relation, val). This specializes in searching single tables, NOT for doing joins. If you want to do joins use search_sets which uses a more complicated graph traversing join algorithm. Oh, and it hasn't been written. =cut sub search { my ($self, %extra) = @_; my $table = $self->table_name; my @where_list; my @where_vals; my @order_list; my @group_list; my @select_cols; # Look for extra-sql parameters push @where_list, "(@{$extra{where}})" if $extra{where}; push @order_list, @{$extra{order_by}} if $extra{order_by}; push @group_list, @{$extra{group_by}} if $extra{group_by}; push @select_cols, @{$extra{select_cols}} if $extra{select_cols}; # Add in our standing group-by and order-by push @group_list, @{$self->{group_by}} if $self->{group_by}; push @order_list, @{$self->{order_by}} if $self->{order_by}; push @select_cols, @{$self->{select_cols}} if $self->{select_cols}; # Set up the where clause # Deal with special array-ref entries foreach my $field (keys %{$self->{currow}}) { if(ref($self->{currow}{$field}) eq 'ARRAY') { my ($relation, $val) = @{$self->{currow}{$field}}; push @where_list, "$field $relation ?"; push @where_vals, $val; } else { # This is just a normal field push @where_list, "$field = ?"; push @where_vals, $self->{currow}{$field}; } } my $select_cols = join ',', @select_cols; $select_cols = '*' unless $select_cols; my $where = join 'AND', @where_list; $where = "WHERE $where" if $where; my $order_by = join ',', @order_list; $order_by = "ORDER BY $order_by" if $order_by; my $group_by = join ',', @group_list; $group_by = "GROUP BY $group_by" if $group_by; my $sql = qq{ SELECT $select_cols FROM $table $where $group_by $order_by }; ODB::debug(2,"SQL: $sql\n"); $self->{results} = $self->conn->prepare($sql); $self->{results}->execute(@where_vals); return $self; } =item $obj->human_search(\@keywords, \@fields) Do a human-like search in this table for the given keywords. Basically, that means that we want ALL of the given words, in ANY column. =cut sub human_search { my ($self, $keywords, $fields) = @_; unless($fields) { my @fields = keys %{$self->column_names}; $fields = \@fields; print STDERR "Fields: @fields\n"; } my $table = $self->table_name; my (@where, @vals); foreach my $keyword (@$keywords) { print STDERR "Keyword: $keyword\n"; my (@where_list, @val_list); foreach my $field (@$fields) { print STDERR "Field: $field\n"; next if $field =~ /_id$/; push @where_list, "$field = ?"; push @val_list, $keyword; } my $where = "(" . (join ' OR ', @where_list) . ")"; print STDERR "Partial where: $where\n"; push @where, $where; push @vals, @val_list; } my $where = join ' AND ', @where; $where = "WHERE $where" if $where; my $sql = "SELECT * FROM $table $where"; ODB::debug(2,"SQL: $sql\n"); $self->{results} = $self->conn->prepare($sql); $self->{results}->execute(@vals); return $self; } =item $obj->next Try to fetch the next row after a $obj->search of some sort =cut sub next { my ($self) = @_; $self->{currow} = $self->{results}->fetchrow_hashref; return $self->{currow}; } sub fetch_all { my ($self) = @_; my @objs; while($self->next) { push @objs, $self->clone; } return @objs; } =item AUTOLOAD -- where all the magic happens! Here we check to see if they are actually referring to one of our columns or relations. If so, we do something magical (call functions that are otherwise available! Crazyness!) =cut sub AUTOLOAD { my ($self, @params) = @_; # $AUTOLOAD is magically defined as the method they tried to invoke, # including the full package name (which we strip) my $func = $AUTOLOAD; $func =~ s/^.*:://; # Someday I might have aliases #$func = $self->{alias}{$func} if $self->{alias}{$func}; # If we are in object or non-list context, give back the column return $self->get_field_obj($func, @params) if want(qw'OBJECT !LIST'); # Otherwise, they probably want a column value or has-a/has-many relation return $self->do_func($func, @params); } # Split this so we can do a non-want-sensitive call if need be sub do_func { my ($self, $func, @params) = @_; # First check for a normal column if($self->column_names->{$func}) { return $self->get_or_set_col($func, @params); # Now look for has_a } elsif($self->has_a->{$func}) { return $self->get_or_set_has_a($func, @params); # Now look for has_many } elsif($self->has_many->{$func}) { return $self->get_or_set_has_many($func, @params); # Some hard-wired cases } elsif($func eq 'DESTROY') { # Do nothing # All other cases are erors! } else { my ($package, $filename, $line) = caller; warn "Error: unknown column/method $func,\n\tat $package ($filename) line $line\n"; } } =back =head1 HTML-Oriented INSTANCE METHODS ************************************* HTML-oriented stuff... will me moved! ************************************* =over =item $obj->html_update($params) =item $obj->html_update($params, $field1, $field2) Will update all the (given) fields from an html hash =cut sub html_update { my ($self, $params, @fields) = @_; unless(@fields) { @fields = keys %{$self->column_names}; } foreach my $field (@fields) { $self->$field->html_update($params); } } =over =cut 1;