DBIO
view release on metacpan or search on metacpan
lib/DBIO/Row.pm view on Meta::CPAN
# (Row.pm's `update`, above).
sub _has_on_column_change_cbs {
my $self = shift;
return !! ( $self->_before_change || $self->_around_change || $self->_after_change );
}
# True if update()'s CascadeActions wrap would actually attempt a cascade for
# THIS row instance, once $upd (if any) has been applied -- mirrors its
# per-relationship skip condition exactly (Relationship::CascadeActions::
# update's own `next if $accessor eq 'single' && !exists
# $self->{_relationship_data}{$rel}`), rather than the coarser "does a
# cascade_update relationship exist on the source at all" question.
#
# A 'single'-accessor relationship (has_one/might_have, the cascade_update
# default) only cascades if its accessor has already been touched on this
# instance: explicitly assigned, or fetched at least once as a getter --
# either way, that's what populates _relationship_data (see
# Relationship::Codegen's 'single' accessor codegen). A freshly-loaded row
# that never touched the accessor has _relationship_data{$rel} unset, so
# CascadeActions itself would skip it -- nothing to cascade, and update_async
# must not punt over it either, or a plain one-column update on any class
# with a has_one/might_have relationship would never be truly async.
#
# $upd is checked too, and must be: set_inflated_columns (called from
# update()'s own body) populates _relationship_data for a 'single'-accessor
# key found in $upd as it processes it -- but that happens AFTER this gate
# would otherwise already have decided not to punt, since update_async calls
# this predicate before ever calling set_inflated_columns on the fast path.
# Without this check, assigning a cascade_update relationship via the update
# hashref itself (update_async({ rel => $obj })), rather than via a prior
# separate accessor call, would silently dispatch truly async and skip the
# cascade sync performs for the exact same call shape.
#
# Both checks are pure hash-key tests -- no accessor call, no I/O, no second
# invocation of set_inflated_columns -- calling the accessor itself to check
# would trigger the very query this predicate exists to let update_async
# avoid.
#
# A cascade_update relationship whose accessor attr is not 'single' (only
# possible via manual configuration; has_one/might_have always use 'single')
# is not skippable in CascadeActions's own logic, so it is always treated as
# cascading here too, matching CascadeActions's unconditional proceed for
# that case.
sub _has_cascade_update_relateds {
my ($self, $upd) = @_;
my $source = $self->result_source;
my %rels = map { $_ => $source->relationship_info($_) } $source->relationships;
for my $rel (grep { $rels{$_}{attrs}{cascade_update} } keys %rels) {
my $accessor = $rels{$rel}{attrs}{accessor} || '';
if ($accessor eq 'single') {
next unless exists $self->{_relationship_data}{$rel}
or ($upd and exists $upd->{$rel} and (!defined $upd->{$rel} or ref $upd->{$rel}));
}
return 1;
}
return 0;
}
sub update_async {
my ($self, $upd) = @_;
return $self->_row_run_async('update',
sub {
my ($backend, $fc) = @_;
# ADR 0071: the hash is an assignment of the caller's
$self->__assert_update_assignable($upd) if $upd;
if (my $pkg = $self->_async_override_gap('update')) {
return $self->_async_ctx_or_sync_punt($fc, 'update',
'an override of update', sub { $self->update($upd) },
$self->_async_override_gap_lead($pkg, 'update'));
}
if ($self->_has_on_column_change_cbs or $self->_has_cascade_update_relateds($upd)) {
return $self->_async_ctx_or_sync_punt($fc, 'update',
'an OnColumnChange callback or cascade_update relationship wraps update()',
sub { $self->update($upd) });
}
# update() calls it first as well; the punts above run the full
# update(), which calls it itself. It may take keys out of $upd.
$self->prepare_update($upd);
$self->{_dirty_columns} ||= {};
delete local @{$self->{_dirty_columns}}{@{$self->_proxy_slots||[]}};
$self->set_inflated_columns($upd) if $upd;
my %to_update = $self->get_dirty_columns;
return $fc->done($self) unless %to_update;
$self->throw_exception( "Not in database" ) unless $self->in_storage;
my $rsrc = $self->result_source;
my $ident_cond = $self->_storage_ident_condition;
return $backend->update_async($rsrc, \%to_update, $ident_cond)->then(sub {
my ($rows) = @_;
if ($rows == 0) {
$self->throw_exception( "Can't update ${self}: row not found" );
} elsif ($rows > 1) {
$self->throw_exception("Can't update ${self}: updated more than one row");
}
$self->{_dirty_columns} = {};
$self->{related_resultsets} = {};
delete $self->{_column_data_in_storage};
$self->store_storage_values if @{$self->storage_value_columns};
return $self;
});
},
sub { $self->update($upd) },
);
}
sub update {
my ($self, $upd) = @_;
# ADR 0071: the hash is an assignment of the caller's
$self->__assert_update_assignable($upd) if $upd;
# whatever a component has to do before the columns are set and written;
# it may take keys out of $upd
$self->prepare_update($upd);
# ProxyResultSetMethod (integrated helper): strip proxy slots from the
# dirty-column set so UPDATE statements never include them. "delete
# local" localises the deletion for the scope of this sub, so the
# original dirty flags are restored when update returns.
$self->{_dirty_columns} ||= {};
delete local @{$self->{_dirty_columns}}{@{$self->_proxy_slots||[]}};
# OnColumnChange (integrated helper): short-circuit when nothing is
# registered. When callbacks exist, build the before/around/after
# chain, then delegate to _update_body via around-wrapper; the whole
# orchestration is a no-op if none of the registered columns are dirty.
my $has_change_cbs =
$self->_before_change
|| $self->_around_change
|| $self->_after_change;
if ($has_change_cbs) {
$self->set_inflated_columns($upd) if $upd;
my %dirty = $self->get_dirty_columns
or return $self;
my @all_before = @{$self->_before_change || []};
my @all_around = @{$self->_around_change || []};
my @all_after = @{$self->_after_change || []};
my @before = grep { defined $dirty{$_->{column}} } @all_before;
my @around = grep { defined $dirty{$_->{column}} } @all_around;
my @after = grep { defined $dirty{$_->{column}} } @all_after;
my $allow_override = $self->on_column_change_allow_override_args;
my $final = $allow_override
? sub { $self->_update_body }
: sub { $self->_update_body($upd) };
for (reverse @around) {
my $fn = $_->{method};
my $old = $self->get_storage_value($_->{column});
my $new = $dirty{$_->{column}};
my $old_final = $final;
$final = sub { $self->$fn($old_final, $old, $new) };
}
my $txn_wrap = List::Util::first {
defined $dirty{$_->{column}} && $_->{txn_wrap}
} @all_before, @all_around, @all_after;
my $guard;
$guard = $self->result_source->schema->txn_scope_guard if $txn_wrap;
for (@before) {
lib/DBIO/Row.pm view on Meta::CPAN
;
if (my ($colname) = sort { length($a) <=> length ($b) } keys %{$prefetch->{$rel_name}[0] || {}} ) {
$err .= sprintf ", check the inflation specification (columns/as) ending in '...%s.%s'",
$rel_name,
$colname,
}
$rsrc->throw_exception($err);
};
$class->throw_exception("No accessor type declared for prefetched relationship '$rel_name'")
unless $relinfo->{attrs}{accessor};
my $rel_rs = $new->related_resultset($rel_name);
my @rel_objects;
if (
@{ $prefetch->{$rel_name} || [] }
and
ref($prefetch->{$rel_name}) ne $DBIO::ResultSource::RowParser::Util::null_branch_class
) {
if (ref $prefetch->{$rel_name}[0] eq 'ARRAY') {
my $rel_rsrc = $rel_rs->result_source;
my $rel_class = $rel_rs->result_class;
my $rel_inflator = $rel_class->can('inflate_result');
@rel_objects = map
{ $rel_class->$rel_inflator ( $rel_rsrc, @$_ ) }
@{$prefetch->{$rel_name}}
;
}
else {
@rel_objects = $rel_rs->result_class->inflate_result(
$rel_rs->result_source, @{$prefetch->{$rel_name}}
);
}
}
if ($relinfo->{attrs}{accessor} eq 'single') {
$new->{_relationship_data}{$rel_name} = $rel_objects[0];
}
elsif ($relinfo->{attrs}{accessor} eq 'filter') {
$new->{_inflated_column}{$rel_name} = $rel_objects[0];
}
$rel_rs->set_cache(\@rel_objects);
}
}
$new->in_storage (1);
# StorageValues (integrated helper): snapshot freshly-inflated values.
$new->store_storage_values if @{$new->storage_value_columns};
return $new;
}
sub insert_or_update { shift->update_or_insert(@_) }
sub update_or_insert {
my $self = shift;
return ($self->in_storage ? $self->update : $self->insert);
}
sub insert_or_update_async { shift->update_or_insert_async(@_) }
sub update_or_insert_async {
my $self = shift;
return ($self->in_storage ? $self->update_async : $self->insert_async);
}
sub is_changed {
return keys %{shift->{_dirty_columns} || {}};
}
sub is_column_changed {
my( $self, $col ) = @_;
return exists $self->{_dirty_columns}->{$col};
}
sub result_source {
$_[0]->throw_exception( 'result_source can be called on instances only' )
unless ref $_[0];
@_ > 1
? $_[0]->{_result_source} = $_[1]
# note this is a || not a ||=, the difference is important
: $_[0]->{_result_source} || do {
my $class = ref $_[0];
$_[0]->can('result_source_instance')
? $_[0]->result_source_instance
: $_[0]->throw_exception(
"No result source instance registered for $class, did you forget to call $class->table(...) ?"
)
}
;
}
sub register_column {
my ($class, $col, $info) = @_;
my $acc = $col;
if (exists $info->{accessor}) {
return unless defined $info->{accessor};
$acc = [ $info->{accessor}, $col ];
}
$class->mk_group_accessors('column' => $acc);
}
sub get_from_storage {
my $self = shift @_;
my $attrs = shift @_;
my $resultset = $self->result_source->resultset;
if(defined $attrs) {
$resultset = $resultset->search(undef, $attrs);
}
# ADR 0045 / karr k325: the lookup itself is synchronous and never reads
# the pin, but the row it returns is derived from this one and keeps it --
# and so does this row when discard_changes refreshes it from that copy.
$resultset->{_async_context} = $self->{_async_context}
( run in 0.866 second using v1.01-cache-2.11-cpan-145b269f2f3 )