From 6cfedfd713a4492b5068044e98f300cb04a718e1 Mon Sep 17 00:00:00 2001 From: David Gustafsson Date: Thu, 29 Sep 2022 17:18:01 +0200 Subject: [PATCH] Bug 32476: Add caching for relatively expensive patron methods To test: 1) Ensure tests in t/db_dependant/Koha/Patrons.t all pass Sponsored-by: Gothenburg University Library --- C4/Circulation.pm | 7 ++- Koha/Object.pm | 87 +++++++++++++++++++++++++++++++++++ Koha/Patron.pm | 50 +++++++++++++++++--- t/db_dependent/Koha/Patrons.t | 61 +++++++++++++++++++++--- 4 files changed, 189 insertions(+), 16 deletions(-) diff --git a/C4/Circulation.pm b/C4/Circulation.pm index c27a4654448..b1968e101eb 100644 --- a/C4/Circulation.pm +++ b/C4/Circulation.pm @@ -1738,7 +1738,6 @@ sub AddIssue { if ( $accumulate_charge > 0 ) { AddIssuingCharge( $issue, $accumulate_charge, 'RENT_DAILY' ); $charge += $accumulate_charge; - $item_unblessed->{charge} = $charge; } } @@ -2946,7 +2945,7 @@ sub CanBookBeRenewed { my $restrictionblockrenewing = C4::Context->preference('RestrictionBlockRenewing'); $patron = Koha::Patrons->find($borrowernumber); # FIXME Is this really useful? my $restricted = $patron->is_debarred; - my $hasoverdues = $patron->has_overdues; + my $hasoverdues = $patron->has_overdues({ cache => 1 }); if ( $restricted and $restrictionblockrenewing ) { return ( 0, 'restriction'); @@ -3236,7 +3235,7 @@ sub AddRenewal { my $overdue_restrictions = $patron->restrictions->search({ type => 'OVERDUES' }); if ( $patron && $patron->is_debarred - && ! $patron->has_overdues + && !$patron->has_overdues && $overdue_restrictions->count ) { DelUniqueDebarment({ borrowernumber => $borrowernumber, type => 'OVERDUES' }); @@ -4456,7 +4455,7 @@ sub _CanBookBeAutoRenewed { } ); - if ( $patron->category->effective_BlockExpiredPatronOpacActions and $patron->is_expired ) { + if ( $patron->is_expired({ cache => 1 }) && $patron->category->effective_BlockExpiredPatronOpacActions ) { return 'auto_account_expired'; } diff --git a/Koha/Object.pm b/Koha/Object.pm index e463f949205..d5b1f06a0a8 100644 --- a/Koha/Object.pm +++ b/Koha/Object.pm @@ -88,6 +88,93 @@ sub new { } +=head3 Koha::Object->_instance_cache_key(); + +my $instance_cache_key = Koha::Object->_instance_cache_key($key); + +=cut + +sub _instance_cache_key { + my ($self, $key) = @_; + warn "_instance_cache_key must not be called on object that has not been stored" unless $self->id; + return $self->_type . ':' . $self->id . ':' . $key; +} + +=head3 Koha::Object->_instance_cache_get(); + +my $value = Koha::Object->_instance_cache_get($cache_key); + +=cut + +sub _instance_cache_get { + my ($self, $key) = @_; + return unless $self->id; + return Koha::Cache::Memory::Lite->get_instance->get_from_cache( + $self->_instance_cache_key($key) + ); +} + +=head3 Koha::Object->_instance_cache_set(); + +Koha::Object->_instance_cache_set($cache_key, $value); + +=cut + +sub _instance_cache_set { + my ($self, $key, $value) = @_; + return unless $self->id; + $self->{_cache}->{$key} = $value; + return Koha::Cache::Memory::Lite->get_instance->set_in_cache( + $self->_instance_cache_key($key), + $value + ); +} + +=head3 Koha::Object->_instance_cache_clear($cache_key); + +Koha::Object->_instance_cache_clear($cache_key); +Koha::Object->_instance_cache_clear(); + +=cut + +sub _instance_cache_clear { + my ($self, $key) = @_; + return unless $self->id; + Koha::Cache::Memory::Lite->get_instance->clear_from_cache( + $self->_instance_cache_key($key) + ) +} + +=head3 Koha::Object->_accessor_cache($method_name); + +my $value = Koha::Object->_accessor_cache($method_name); + +=cut + +sub _accessor_cache { + my ($self, $method_name) = @_; + + my $value = $self->_instance_cache_get($method_name); + + return $value if defined $value; + + $value = $self->$method_name(); + $self->_instance_cache_set($method_name, $value); + + return $value; +} + +=head3 Koha::Object->accessor_cache_clear($method_name); + +Koha::Object->accessor_cache_clear($method_name); + +=cut + +sub accessor_cache_clear { + my ($self, $method_name) = @_; + $self->_instance_cache_clear($method_name); +} + =head3 Koha::Object->_new_from_dbic(); my $object = Koha::Object->_new_from_dbic($dbic_row); diff --git a/Koha/Patron.pm b/Koha/Patron.pm index f0e17342171..e08e04604ee 100644 --- a/Koha/Patron.pm +++ b/Koha/Patron.pm @@ -776,20 +776,53 @@ sub is_debarred { return; } +=head3 dateexpiry + +Override Patron::dateexpiry to invalidate Patron::is_expired cache. + +=cut + +sub dateexpiry { + my $self = shift @_; + if (@_) { + $self->_instance_cache_clear('is_expired'); + } + return $self->SUPER::dateexpiry(@_); +} + +=head3 set + +Override Patron::set to invalidate Patron::is_expired cache if dateexpiry is set. + +=cut + +sub set { + my $self = shift @_; + my ($properties) = @_; + if (exists $properties->{dateexpiry}) { + $self->_instance_cache_clear('is_expired'); + } + return $self->SUPER::set(@_); +} + =head3 is_expired my $is_expired = $patron->is_expired; +my $is_expired = $patron->is_expired({ cache => 1}); Returns 1 if the patron is expired or 0; =cut sub is_expired { - my ($self) = @_; - return 0 unless $self->dateexpiry; - return 0 if $self->dateexpiry =~ '^9999'; - return 1 if dt_from_string( $self->dateexpiry ) < dt_from_string->truncate( to => 'day' ); - return 0; + my ($self, $params) = @_; + $params //= {}; + return $self->_accessor_cache('is_expired') if $params->{cache}; + + $self->_instance_cache_clear('is_expired'); + return $self->dateexpiry && + $self->dateexpiry !~ '^9999' && + dt_from_string( $self->dateexpiry ) < dt_from_string->truncate( to => 'day' ) ? 1 : 0; } =head3 password_expired @@ -987,13 +1020,18 @@ sub renew_account { =head3 has_overdues my $has_overdues = $patron->has_overdues; +my $has_overdues = $patron->has_overdues({ cache => 1 }); Returns the number of patron's overdues =cut sub has_overdues { - my ($self) = @_; + my ($self, $params) = @_; + $params //= {}; + return $self->_accessor_cache('has_overdues') if $params->{cache}; + + $self->_instance_cache_clear('has_overdues'); my $dtf = Koha::Database->new->schema->storage->datetime_parser; return $self->_result->issues->search({ date_due => { '<' => $dtf->format_datetime( dt_from_string() ) } })->count; } diff --git a/t/db_dependent/Koha/Patrons.t b/t/db_dependent/Koha/Patrons.t index 187b18c3799..aed876531d5 100755 --- a/t/db_dependent/Koha/Patrons.t +++ b/t/db_dependent/Koha/Patrons.t @@ -264,7 +264,7 @@ subtest 'siblings' => sub { }; subtest 'has_overdues' => sub { - plan tests => 3; + plan tests => 12; my $item_1 = $builder->build_sample_item; my $retrieved_patron = Koha::Patrons->find( $new_patron_1->borrowernumber ); @@ -273,26 +273,75 @@ subtest 'has_overdues' => sub { my $tomorrow = DateTime->today( time_zone => C4::Context->tz() )->add( days => 1 ); my $issue = Koha::Checkout->new({ borrowernumber => $new_patron_1->id, itemnumber => $item_1->itemnumber, date_due => $tomorrow, branchcode => $library->{branchcode} })->store(); is( $retrieved_patron->has_overdues, 0, ); + + # Cache is set + is( $retrieved_patron->has_overdues({ cache => 1}), 0, ); + is( $retrieved_patron->_instance_cache_get('has_overdues'), 0, 'has_overdues cache has been set'); + # Cache is used + is( $retrieved_patron->has_overdues({ cache => 1}), 0, 'has_overdues cache is used'); + $issue->delete(); my $yesterday = DateTime->today(time_zone => C4::Context->tz())->add( days => -1 ); $issue = Koha::Checkout->new({ borrowernumber => $new_patron_1->id, itemnumber => $item_1->itemnumber, date_due => $yesterday, branchcode => $library->{branchcode} })->store(); $retrieved_patron = Koha::Patrons->find( $new_patron_1->borrowernumber ); is( $retrieved_patron->has_overdues, 1, ); + is( $retrieved_patron->_instance_cache_get('has_overdues'), undef, 'has_overdues cache has been invalidated' ); + # Cache is set + is( $retrieved_patron->has_overdues({ cache => 1}), 1, ); + is ( $retrieved_patron->_instance_cache_get('has_overdues'), 1, 'has_overdues cache has been set' ); + # Cache is used + is( $retrieved_patron->has_overdues({ cache => 1}), 1, 'has_overdues cache is used'); + $issue->delete(); + + # Cache is used (with stale entry, flushing cache must be handled manually as automatic invalidation is + # complex and probably not worth the effort) + is( $retrieved_patron->has_overdues({ cache => 1}), 1, ); + + # Cache has been invalidated + is( $retrieved_patron->has_overdues, 0, ); }; subtest 'is_expired' => sub { - plan tests => 4; + plan tests => 15; my $patron = $builder->build({ source => 'Borrower' }); $patron = Koha::Patrons->find( $patron->{borrowernumber} ); $patron->dateexpiry( undef )->store->discard_changes; - is( $patron->is_expired, 0, 'Patron should not be considered expired if dateexpiry is not set'); + is( $patron->is_expired, 0, 'Patron should not be considered expired if dateexpiry is not set' ); $patron->dateexpiry( dt_from_string )->store->discard_changes; - is( $patron->is_expired, 0, 'Patron should not be considered expired if dateexpiry is today'); + is( $patron->is_expired, 0, 'Patron should not be considered expired if dateexpiry is today' ); $patron->dateexpiry( dt_from_string->add( days => 1 ) )->store->discard_changes; - is( $patron->is_expired, 0, 'Patron should not be considered expired if dateexpiry is tomorrow'); + is( $patron->is_expired, 0, 'Patron should not be considered expired if dateexpiry is tomorrow' ); + $patron->dateexpiry( dt_from_string->add( days => -1 ) )->store->discard_changes; + is( $patron->is_expired, 1, 'Patron should be considered expired if dateexpiry is yesterday' ); + + # Test is_expired cache + $patron->dateexpiry( undef )->store->discard_changes; + # Set cache + is( $patron->is_expired({ cache => 1 }), 0, 'Patron should not be considered expired if dateexpiry is not set, with caching enabled' ); + is( $patron->_instance_cache_get('is_expired'), 0, 'is_expired cache has been set'); + is( $patron->is_expired({ cache => 1 }), 0, 'Patron should not be considered expired if dateexpiry is not set, with caching enabled and cache hit' ); $patron->dateexpiry( dt_from_string->add( days => -1 ) )->store->discard_changes; - is( $patron->is_expired, 1, 'Patron should be considered expired if dateexpiry is yesterday'); + is( $patron->is_expired({ cache => 1 }), 1, 'Patron should be considered expired if dateexpiry is yesterday, cache has been invalidated' ); + is( $patron->_instance_cache_get('is_expired'), 1, 'is_expired cache has been set'); + is( $patron->is_expired({ cache => 1 }), 1, 'Patron should be considered expired if dateexpiry is yesterday, on cache hit' ); + + $patron->{dateexpiry} = undef; + # Checking cache is really utilized by setting dateexpiry property without using accessor method + is( $patron->is_expired({ cache => 1 }), 1, 'Cache is used with stale value' ); + + $patron->set({ dateexpiry => undef })->store->discard_changes; + is( $patron->is_expired({ cache => 1 }), 0, 'Patron should not be considered expired if dateexpiry is not set, cache has been invalidated' ); + is( $patron->is_expired({ cache => 1 }), 0, 'Patron should not be considered expired if dateexpiry is not set, on cache hit' ); + + $patron->is_expired; + is( $patron->_instance_cache_get('is_expired'), undef , 'Cache has been invalidated after calling is_expired without caching enabled' ); + + # Set cache + $patron->is_expired({ cache => 1 }); + # Clear cache using public method accessor_cache_clear + $patron->accessor_cache_clear('is_expired'); + is( $patron->_instance_cache_get('is_expired'), undef , 'Cache has been invalidated after calling accessor_cache_clear' ); $patron->delete; }; -- 2.40.0