Bivio::Biz::PropertyModel
# Copyright (c) 1999-2010 bivio Software, Inc. All rights reserved. # $Id$ package Bivio::Biz::PropertyModel; use strict; use Bivio::Base 'Biz.Model'; b_use('IO.Trace'); our($_TRACE); my($_PI) = b_use('Type.PrimaryId'); my($_PS) = b_use('SQL.PropertySupport'); my($_HANDLERS) = b_use('Biz.Registrar')->new; b_use('Bivio.Cache')->init; sub assert_properties { my($self, $values) = @_; foreach my $k (@{$self->get_info('column_names')}) { $self->die($k, ': missing column') unless exists($values->{$k}); } return $values; } sub cascade_delete { my($self, $query) = @_; my($support) = $self->internal_get_sql_support; # DEPRECATED: cascade_delete_children is not used anywhere my($method) = $support->get('cascade_delete_children') ? 'cascade_delete' : 'delete_all'; my($properties) = $query || $self->get_shallow_copy; foreach my $c (@{$support->get_children}) { my($child) = $self->new_other($c->[0]); my($key_map) = $c->[1]; my($child_query) = { map({ my($ck) = $key_map->{$_}; exists($properties->{$ck}) ? ($_ => $properties->{$ck}) : (); } keys(%$key_map)), }; $child->$method($child_query) if %$child_query; } $query ? $self->delete_all($query) : $self->delete; return; } sub create { my($self, $new_values) = @_; # Creates a new model in the database with the specified values. After creation, # this instance takes ownership of I<new_values>. Dies on error. # # Returns I<self>. $new_values = _dup($new_values); my($sql_support) = $self->internal_get_sql_support; # Make sure all columns are defined my($n); foreach $n (@{$sql_support->get('column_names')}) { $new_values->{$n} = undef unless exists($new_values->{$n}); } $sql_support->create($new_values, $self); $self->internal_load_properties($new_values); $self->internal_data_modification(create => $new_values); return $self; } sub create_from_literals { my($self, $values) = @_; # Converts and validates I<values> calling # L<Bivio::Type::from_literal_or_die|Bivio::Type/"from_literal_or_die"> # for types of I<values>. # # B<Note: I<from_literal_or_die> dies on NULL.> # # Only validates keys which exist, i.e. primary keys or values which # are defaulted by L<create|"create"> are not validated. $values = _dup($values); while (my($k, $v) = each(%$values)) { $values->{$k} = $self->get_field_type($k)->from_literal_or_die($v); } return $self->create($values); } sub create_or_unauth_update { # B<DEPRECATED> Bivio::IO::Alert->warn_deprecated('Use unauth_create_or_update instead.'); return shift->unauth_create_or_update(@_); } sub create_or_update { my($self, $new_values) = @_; # Tries to load the model based on its primary key values. # If the load is successful, the model will be updated with # the new values. Otherwise, a new model is created. # # Adds auth_id to I<new_values>. # # See also L<unauth_create_or_update|"unauth_create_or_update">. return $self->unauth_create_or_update(_add_auth_id($self, $new_values)); } sub delete { my($self) = @_; # Deletes the current model from the database. Dies on error. # # If I<load_args> is supplied, deletes the model specified by # I<load_args> and not the current model. May be called statically. my($query, $l); if (@_ <= 1) { $l = $query = $self->internal_get; } else { ($self, $query) = _load_args(@_); $self = $self->new() unless ref($self); $l = $self->internal_prepare_query($query = _add_auth_id($self, $query)); } $self->internal_data_modification(delete => $query); return $self->internal_get_sql_support->delete($l, $self); } sub delete_all { my($self, $query) = @_; Bivio::IO::Alert->warn_deprecated('missing query args') unless $query && %$query; # Deletes all the models of this type with the specified (possibly # partial key) query. Returns the number of models deleted. $self = $self->new unless ref($self); # Should not be a lot except in test case $query = _add_auth_id($self, $query); $self->internal_data_modification(delete_all => $query); my($rows) = $self->internal_get_sql_support->delete_all( $self->internal_prepare_query($query), $self, ); _trace($rows, ' ', ref($self)) if $_TRACE; return $rows; } sub execute_auth_user { my($proto, $req) = @_; # Loads this auth user's data as if realm_id. $proto->new($req)->load_for_auth_user; return 0; } sub execute_load { my($proto, $req) = @_; # Loads this model using no params except auth_id. $proto->new($req)->load(); return 0; } sub execute_load_parent { my($proto, $req) = @_; # Loads a new instance of this class using the request using # the "parent" on the request query. # See L<load_parent_from_request|"load_parent_from_request"> $proto->new($req)->load_parent_from_request(); return 0; } sub execute_load_this { my($proto, $req) = @_; # Loads a new instance of this class using the request using # the "this" on the request. # See L<load_this_from_request|"load_this_from_request"> $proto->new($req)->load_this_from_request(); return 0; } sub execute_unauth_delete_this { my($proto, $req) = @_; # Deletes the "this" model by calling get_model for this model. # Deletes "this" from list query and query. my($lm) = $req->get('list_model'); $lm->set_cursor_or_not_found(0); $lm->get_model($proto->simple_package_name)->unauth_delete; $lm->get_query->delete('this'); delete($req->get('query')->{Bivio::SQL::ListQuery->to_char('this')}); return; } sub execute_unauth_load_this { my($proto, $req) = @_; # Loads a new instance of this class using the request using # the "this" on the request. # See L<unauth_load_this_from_request|"unauth_load_this_from_request"> my($self) = $proto->new($req); $self->throw_die('MODEL_NOT_FOUND') unless $self->unauth_load_this_from_request; return 0; } sub format_query_for_parent { my($self) = @_; # Query string used to identify this instance using "parent" key, # so can be used with I<load_parent_from_request>. return Bivio::SQL::ListQuery->format_uri_for_this_as_parent( $self->internal_get_sql_support, $self->internal_get); } sub format_query_for_this { my($self, $query) = @_; # Query string used to identify this instance. If supplied I<load_query>, # must contain primary keys for the model. return Bivio::SQL::ListQuery->format_uri_for_this( $self->internal_get_sql_support, $query || $self->internal_get); } sub get_keys { # B<DEPRECATED> # # Returns a copy of the I<column_names> attribute. # # TODO: Need to move this up to Model, but I think this might break ListModel and # FormModel, because get_keys returns all keys and column_names only returns # declared columns. Some models don't declare all their columns. return [@{shift->get_info('column_names')}]; } sub get_qualified_field_name { # Returns qualified field name (Model.field) for field. return shift->simple_package_name . '.' . shift(@_); } sub get_qualified_field_name_list { my($self) = @_; return map($self->get_qualified_field_name($_), @{$self->get_keys}); } sub generate_unique_for_field { my($self, $field, $generate_method, $check_method) = @_; $generate_method ||= 'generate'; $check_method ||= 'unauth_load'; my($type) = $self->get_field_type($field); my($value); my($i) = 0; until ($value) { b_die($type, ': infinite loop generating value') if ++$i > 10; my($v) = $type->$generate_method; $value = ref($check_method) eq 'CODE' ? $check_method->($v) : $self->$check_method({ $field => $v, }) ? undef : $v; } return $value; } sub internal_data_modification { my($self, $op, $query) = @_; $_HANDLERS->call_fifo( handle_property_model_modification => [$self, $op, {%{$query || {}}}]); return; } sub internal_get_target { my($self, $model, $model_prefix, $values) = @_; # Returns the class, target model and optional model prefix. This method is used # by subclasses when defining a method which can operate on self, on another # model target, or $values. For an example, see RealmOwner.format_email. # If values is undef, internal_get is called on the model. $model ||= $self; return ( ref($self) || $self, $model, $model_prefix || '', $values || $model->internal_get, ); } sub internal_initialize { return {}; } sub internal_initialize_sql_support { my($proto, $stmt, $config) = @_; # Returns the L<Bivio::SQL::PropertySupport|Bivio::SQL::PropertySupport> # for this class. Calls L<internal_initialize|"internal_initialize"> # to get the hash_ref to initialize the sql support instance. die('cannot create anonymous PropertyModels') if $config; $config = $proto->internal_initialize; $config->{class} = ref($proto) || $proto; my($support) = $_PS->new($config); _register_with_parents($proto, $support); return $support; } sub internal_load_properties { my($self, $values, $ephemeral) = @_; # Loads model with values as properties. DOES NOT MAKE A COPY of values. $self->internal_clear_model_cache; $self->assert_properties($values) unless __PACKAGE__ eq (caller)[0]; $self->internal_put($values); $self->put_on_request unless $ephemeral || $self->is_ephemeral; return $self; } sub internal_prepare_query { # Returns I<query> after fixing it up. shift; return shift; } sub internal_unique_load_values { my($self, $values) = @_; # Returns hash_ref which is extracted from $values to unauth_load a # unique row in the table. These are not the primary keys. If those # are available in $values, they will be extracted first. However, # typically, they are not available, and you want to load by another # externally provided value, e.g. RealmOwner.name, which is not the primary key # of the RealmOwner table. return; } sub internal_unload { my($self) = @_; # Clears the model state, if loaded. Deletes from request. return $self unless $self->is_loaded; _unload($self, 1); return $self; } sub is_loaded { # Returns true if the model is loaded (or created), i.e. contains # valid values. # If we have values, we're loaded. return %{shift->internal_get} ? 1 : 0; } sub iterate_next_and_load { my($self) = shift; # Will iterate to the next row and load the model with the row. # Can be used to update a row. # # Returns false if there is no next. # # B<Deprecated form accepts an iterator as the first argument.> my(undef, $row) = $self->internal_iterate_next(@_, {}); return $row ? _load($self, $row, 1) : _unload($self, 1); } sub iterate_start { my($self, $order_by, $query) = _iterate_params(@_); # Begins an iteration which can be used to iterate the rows for this # realm with L<iterate_next|"iterate_next">, # L<iterate_next_and_load|"iterate_next_and_load">, or # L<iterate_next_and_load_new|"iterate_next_and_load_new">. # L<iterate_end|"iterate_end"> should be called when you are through # with the iteration. # # I<order_by> is an SQL C<ORDER BY> clause without the keywords C<ORDER BY>. # # I<query> is the same as in L<load|"load">. # # B<Deprecated form returns the iterator.> $self->throw_die('DIE', 'no auth_id') unless $self->get_request->get('auth_id'); return $self->unauth_iterate_start( $order_by, _add_auth_id($self, $query || {})); } sub load { my($self) = shift; # Loads the model or dies if not found or other error. # Subclasses shouldn't override this method. # # Note: I<query> may be modified by this method. # # Returns I<self>. # # # B<DEPRECATED> return $self if $self->unsafe_load(@_); _die_not_found($self, \@_, caller); # DOES NOT RETURN } sub load_for_auth_user { my($self) = @_; # Loads the model for auth_user. return $self->unauth_load_or_die({ realm_id => ( $self->get_request->get('auth_user') || $self->die('no auth_user') )->get('realm_id'), }); } sub load_from_properties { my($self, $values) = @_; my($support) = $self->internal_get_sql_support; return $self->internal_load_properties( $self->assert_properties({ map({ my($cn) = $support->extract_column_name($_); $support->has_columns($cn) ? ($cn => $values->{$_}) : (); } keys(%$values)), }), ); } sub load_parent_from_request { my($self) = @_; # Parses the query from the request (or list_model) and then L<load|"load">. # Uses "parent" key in query or list model. # # See also L<unsafe_load_parent_from_request|"unsafe_load_parent_from_request">. my($q) = _parse_query($self, 1); $self->throw_die(Bivio::DieCode::CORRUPT_QUERY(), {message => 'see previous warning, too'}, caller) unless $q; return $self->load($q); } sub load_this_from_request { my($self) = @_; # Parses the query from the request (or list_model) and then L<load|"load">. # Uses "this" key in query or list model. # # See also L<unsafe_load_this_from_request|"unsafe_load_this_from_request">. my($q) = _parse_query($self, 0); $self->throw_die(Bivio::DieCode::CORRUPT_QUERY(), {message => 'see previous warning, too'}, caller) unless $q; return $self->load($q); } sub merge_initialize_info { my($proto, $parent, $child) = @_; Bivio::Die->die('columns, if defined, must be a hash_ref') unless ref($parent->{columns} || {}) eq 'HASH' && ref($child->{columns} || {}) eq 'HASH'; return shift->SUPER::merge_initialize_info($parent, { %$child, columns => { %{delete($parent->{columns}) || {}}, %{$child->{columns} || {}}, }, }); } sub new { my($self) = shift->SUPER::new(@_); # Create a new PropertyModel associated with the request. _unload($self, 0); return $self; } sub register_child_model { my($self, $class) = @_; return if $self->simple_package_name eq $class; return shift->internal_get_sql_support_no_assert ->register_child_model(@_); } sub register_handler { shift; $_HANDLERS->push_object(@_); return; } sub rows_exist { my($self, $query) = @_; return $self->unauth_rows_exist(_add_auth_id($self, $query)); } sub test_unauth_delete_all { my($self, $query) = @_; $self->req->assert_test; $self->internal_data_modification(delete_all => $query); my($res) = $self->internal_get_sql_support->delete_all( $self->internal_prepare_query(_dup($query)), $self, ); return $res; } sub unauth_create_or_update { my($self, $new_values) = @_; # Tries to load the model based on its primary key values. # If the load is successful, the model will be updated with # the new values. Otherwise, a new model is created. # # Calls L<unauth_load|"unauth_load">. # # See also L<create_or_update|"create_or_update">. my($pk_values) = _get_primary_keys($self, $new_values) || $self->internal_unique_load_values($new_values); return $pk_values && $self->unauth_load($pk_values) ? $self->update($new_values) : $self->create($new_values); } sub unauth_create_or_update_keys { my($self, $values, $load_pkeys) = @_; # Create model or update values that may include primary keys. # I<load_pkeys> is the set of primary key/value pairs to use when # trying to load the model. # If I<load_pkeys> is supplied, deletes the model specified by # I<load_pkeys> and not the current model. return $self->unauth_create_or_update($values) unless grep({exists($values->{$_})} @{$self->get_info('primary_key_names')}); $self->unauth_load({ map({$_ => exists($load_pkeys->{$_}) ? $load_pkeys->{$_} : $values->{$_} } @{$self->get_info('primary_key_names')}), }) unless $load_pkeys; if ($self->is_loaded()) { foreach my $field (@{$self->get_info('column_names')}) { $values->{$field} = $self->get($field) unless exists($values->{field}); } $self->delete(); } return $self->create($values); } sub unauth_create_unless_exists { my($self, $values) = @_; my($pk_values) = _get_primary_keys($self, $values) || $self->internal_unique_load_values($values); b_die($values, ': no primary key values') unless $pk_values; return $self if $self->unauth_load($pk_values); return $self->create($values); } sub unauth_delete { my($self, $load_args) = @_; # Deletes the current model from the database. Doesn't check # auth_id. Dies on error. # # If I<load_args> is supplied, deletes the model specified by # I<load_args> and not the current model. May be called statically. # Just like L<unauth_load|"unauth_load">. # # Note: I<query> may be modified by this method. $load_args ||= $self->internal_get; $self = $self->new() unless ref($self); $self->die('load_args or model must not be empty') unless %$load_args; $self->internal_data_modification(delete => $load_args); my($res) = $self->internal_get_sql_support->delete( $self->internal_prepare_query({%$load_args})); return $res; } sub unauth_delete_by_realm_id { my($self, $field, $auth_id) = @_; _assert_realm_id_field($self, $field); $self->new->do_iterate( sub { shift->unauth_delete; return 1; }, 'unauth_iterate_start', {$field => $auth_id}, ); return; } sub unauth_iterate_start { my($self, $order_by, $query) = _iterate_params(@_); # B<Do not use this method unless you are sure the user is authorized # to access all realms or all rows of the table.> # # Begins an iteration which can be used to iterate the rows for this # realm with L<iterate_next|"iterate_next">, # L<iterate_next_and_load|"iterate_next_and_load">, or # L<iterate_next_and_load_new|"iterate_next_and_load_new">. # L<iterate_end|"iterate_end"> should be called when you are through # with the iteration. # # I<order_by> is an SQL C<ORDER BY> clause without the keywords # C<ORDER BY>. # # I<query> is the same as in L<load|"load">. # # B<Deprecated form returns the iterator.> return $self->internal_put_iterator( $self->internal_get_sql_support->iterate_start( $self, $order_by, $self->internal_prepare_query($query), ), ); } sub unauth_load { my($self, $query) = _load_args(@_); # Loads the model as with L<unsafe_load|"unsafe_load">. However, does # not insert security realm into query params. Use this when you # B<are certain> there are no security issues involved with loading # the data. # # On success, saves model in request and returns true. # # Returns false if not found. Dies on any other errors. # # Subclasses should override this method if there model doesn't match # the usual property model. L<unsafe_load|"unsafe_load"> and # L<load|"load"> call this method. # # Note: I<query> may be modified by this method. # # # B<DEPRECATED> # Don't bother checking query. Will kick back if empty. my($values) = $self->internal_get_sql_support->unsafe_load( $self->internal_prepare_query(_dup($query)), $self, ); return $values ? _load($self, $values) : _unload($self, 1); } sub unauth_load_or_die { my($self) = shift; # See L<unauth_load|"unauth_load"> for params. Throws a C<MODEL_NOT_FOUND> # exception if the load fails. # # Returns I<self>. # # # B<DEPRECATED> return $self if $self->unauth_load(@_); _die_not_found($self, \@_, caller); # DOES NOT RETURN } sub unauth_load_parent_from_request { my($self) = @_; # Parses the query from the request (or list_model) and then # L<unauth_load|"unauth_load">. If there is no query or the query is # corrupt, returns false. # # See also L<load_parent_from_request|"load_parent_from_request">. my($q) = _parse_query($self, 1); return $q ? $self->unauth_load($q) : 0; } sub unauth_load_self { my($self) = @_; shift->unauth_load(@_); return $self; } sub unauth_load_this_from_request { my($self) = @_; # Parses the query from the request (or list_model) and then # L<unauth_load|"unauth_load">. If there is no query or the query is # corrupt, returns false. # # See also L<load_this_from_request|"load_this_from_request">. my($q) = _parse_query($self, 0); return $q ? $self->unauth_load($q) : 0; } sub unauth_rows_exist { my($self, $query) = @_; my($res) = 0; $self->do_iterate( sub {$res++}, 'unauth_iterate_start', $self->get_info('primary_key_names')->[0], $query, ); return $res; } sub unsafe_load { my($self, $query) = _load_args(@_); # Loads the model. On success, saves model in request and returns true. # # Returns false if not found. Dies on all other errors. # # Subclasses shouldn't override this method. # # B<This method will be dynamically overridden. See # L<internal_initialize_sql_support|"internal_initialize_sql_support">>. # # Note: I<query> may be modified by this method. # # # B<DEPRECATED> return $self->unauth_load(_add_auth_id($self, $query)); } sub unsafe_load_first { my($self) = shift; $self->iterate_start(@_); my($ok) = $self->iterate_next_and_load; $self->iterate_end; return $ok ? $self : undef; } sub unsafe_load_parent_from_request { my($self) = @_; # Loads this model from the parent value in the query on the request. my($q) = _parse_query($self, 1); return $q ? $self->unsafe_load($q) : 0; } sub unsafe_load_this_from_request { my($self) = @_; # Parses the query from the request (or list_model) and then # L<unsafe_load|"unsafe_load">. If there is no query or the query is corrupt, # returns false. # # See also L<load_this_from_request|"load_this_from_request">. my($q) = _parse_query($self, 0); return $q ? $self->unsafe_load($q) : 0; } sub update { my($self, $new_values) = @_; # Updates the current model's values. # NOTE: load() should be called prior to an update. # # Returns I<self>. $new_values = _dup($new_values); b_die('model is not loaded') unless $self->is_loaded; $self->internal_clear_model_cache; my($properties) = $self->internal_get; $self->internal_get_sql_support->update($properties, $new_values, $self); foreach my $n (keys(%$new_values)) { $properties->{$n} = $new_values->{$n}; } $self->internal_data_modification(update => $new_values); return $self; } sub _add_auth_id { my($self, $query) = @_; # Adds the auth_id field and value to the query, if defined. Returns # the query. # # Ensure we are only accessing data from the realm we are authorized # to operate in. $query = _dup($query); my($sql_support) = $self->internal_get_sql_support; my($auth_field) = $sql_support->get('auth_id'); # Warn if we are overriding an existing value for auth_id if ($auth_field) { my($id) = $self->get_request->get('auth_id'); my($n) = $auth_field->{name}; Bivio::IO::Alert->warn_deprecated( $self, ": overriding $n=$query->{$n} in query with auth_id=$id", " from request. You might need to call an unauth_* method instead" ) if exists($query->{$n}) && $query->{$n} ne $id; # Bivio::Die->die() if exists($query->{$n}) && $query->{$n} ne $id; $query->{$n} = $id; } return $query; } sub _assert_realm_id_field { my($self, $field) = @_; if (my $aid = $self->internal_get_sql_support->get('auth_id')) { return if $aid->{name} eq $field; } my($which) = $self; foreach my $sanity (0 .. 20) { # dies if there's no parent my($pf) = $which->get_field_info($field, 'parent_field'); my($pm) = $which->get_field_info($field, 'parent_model'); return if $pm eq 'RealmOwner' || $pf eq 'realm_id'; $which = $which->get_instance($pm); } $self->die($field, ': not a RealmOwner.realm_id'); # DOES NOT RETURN } sub _default_order_by { return join( ',', map($_->{sql_name} . ' ' . ($_->{sort_order} ? 'ASC' : 'DESC'), @{shift->get_info('primary_key')})); } sub _die_not_found { my($self, $args, $pkg, $file, $line) = @_; # Dies with appropriate exception. ($self, $args) = _load_args($self, @$args); $self->throw_die(Bivio::DieCode->MODEL_NOT_FOUND, $args, $pkg, $file, $line); # DOES NOT RETURN } sub _dup { my($v) = @_; return $v ? {%$v} : {}; } sub _get_primary_keys { my($self, $new_values) = @_; # If new_values contains all primary keys, returns a copy as a hash_ref. # Else returns undef. my(%pk_values); my($have_keys) = 1; foreach my $pk (@{$self->get_info('primary_key_names')}) { unless (exists($new_values->{$pk})) { $have_keys = 0; _trace($pk, ': missing primary key') if $_TRACE; last; } $pk_values{$pk} = $new_values->{$pk}; } return $have_keys ? \%pk_values : undef; } sub _iterate_params { my($self, $order_by, $query) = @_; ($order_by, $query) = (undef, $order_by) if ref($order_by) eq 'HASH'; return ($self, $order_by || _default_order_by($self), _dup($query)); } sub _load { # Initializes the self with values and returns 1. shift->internal_load_properties(@_); return 1; } sub _load_args { my($self) = shift; # Parses load args and returns ($self, $query). return ($self, (int(@_) == 1 ? @_ : {@_})); } sub _parse_query { my($self, $want_parent) = @_; # Does work of unsafe_load_this/parent_from_request. my($req) = $self->get_request; my($support) = $self->internal_get_sql_support; my($list_model, $q) = $req->unsafe_get('list_model', 'query'); # Pass a copy of the query, because it is trashed by ListQuery. my($query) = $list_model ? $list_model->get_query() : $q ? Bivio::SQL::ListQuery->new({ %$q, auth_id => $req->get('auth_id'), count => 1}, $support, $self) : undef; # No query is not a _query_err return undef unless $query; # Use this if available, else parent_id my($key); my($pk_cols) = $support->get('primary_key'); if ($want_parent) { my($parent_id) = $query->get('parent_id'); # parent_id has some restrictions, check them #TODO: why can't we have a _query_err here. Too many messages otherwise. return undef unless $parent_id; #TODO: Need to make this work more cleanly my($i) = int(@$pk_cols); die('expecting one or two primary key columns for parent_id') if $i > 2; if ($i == 1) { $key = [$parent_id]; } else { #TODO: Make this cleaner # This is a hack. We need to add in the auth_id to the # query so that the code creating @query works. However, # load overrides auth_id always. my($auth_col) = $support->get('auth_id'); my($auth_id) = $req->get('auth_id'); if ($auth_col->{name} eq $pk_cols->[0]->{name}) { $key = [$auth_id, $parent_id]; } elsif ($auth_col->{name} eq $pk_cols->[1]->{name}) { $key = [$parent_id, $auth_id]; } else { # Should never happen, of course die('(auth_id, parent_id) not primary key'); } } } else { $key = $query->get('this'); return _query_err($self, 'missing this') unless $key; } # Create the query acceptable to load (which always adds auth_id) my(%query) = (); my($i) = 0; foreach my $col (@$pk_cols) { return _query_err($self, "column $col->{name} is NULL") unless defined($key->[$i]); $query{$col->{name}} = $key->[$i++]; } return \%query; } sub _query_err { my($self, $msg) = @_; # Outputs a warning and returns undef. $self->get_request->warn($self, ' query error: ', $msg); return undef; } sub _register_with_parents { my($proto, $support) = @_; my($class, $parents) = $support->get(qw(class parents)); while (my($parent, $key_map) = each(%$parents)) { $proto->get_instance($parent) ->register_child_model($class, $key_map); } return; } sub _unload { my($self, $delete_from_request) = @_; # Always returns false. $self->internal_clear_model_cache; $self->internal_put({}); $self->delete_from_request if $delete_from_request && !$self->is_ephemeral; return 0; } 1;