summaryrefslogtreecommitdiffstats
path: root/Bugzilla/Object.pm
diff options
context:
space:
mode:
Diffstat (limited to 'Bugzilla/Object.pm')
-rw-r--r--Bugzilla/Object.pm355
1 files changed, 306 insertions, 49 deletions
diff --git a/Bugzilla/Object.pm b/Bugzilla/Object.pm
index d4574abd2..8a7bba1c5 100644
--- a/Bugzilla/Object.pm
+++ b/Bugzilla/Object.pm
@@ -47,6 +47,15 @@ use constant AUDIT_CREATES => 1;
use constant AUDIT_UPDATES => 1;
use constant AUDIT_REMOVES => 1;
+# When USE_MEMCACHED is true, the class is suitable for serialisation to
+# Memcached. See documentation in Bugzilla::Memcached for more information.
+use constant USE_MEMCACHED => 1;
+
+# When IS_CONFIG is true, the class is used to track seldom changed
+# configuration objects. This includes, but is not limited to, fields, field
+# values, keywords, products, classifications, priorities, severities, etc.
+use constant IS_CONFIG => 0;
+
# This allows the JSON-RPC interface to return Bugzilla::Object instances
# as though they were hashes. In the future, this may be modified to return
# less information.
@@ -59,8 +68,44 @@ sub TO_JSON { return { %{ $_[0] } }; }
sub new {
my $invocant = shift;
my $class = ref($invocant) || $invocant;
- my $object = $class->_init(@_);
- bless($object, $class) if $object;
+ my $param = shift;
+
+ my $object = $class->_object_cache_get($param);
+ return $object if $object;
+
+ my ($data, $set_memcached);
+ if (Bugzilla->memcached->enabled
+ && $class->USE_MEMCACHED
+ && ref($param) eq 'HASH' && $param->{cache})
+ {
+ if (defined $param->{id}) {
+ $data = Bugzilla->memcached->get({
+ table => $class->DB_TABLE,
+ id => $param->{id},
+ });
+ }
+ elsif (defined $param->{name}) {
+ $data = Bugzilla->memcached->get({
+ table => $class->DB_TABLE,
+ name => $param->{name},
+ });
+ }
+ $set_memcached = $data ? 0 : 1;
+ }
+ $data ||= $class->_load_from_db($param);
+
+ if ($data && $set_memcached) {
+ Bugzilla->memcached->set({
+ table => $class->DB_TABLE,
+ id => $data->{$class->ID_FIELD},
+ name => $data->{$class->NAME_FIELD},
+ data => $data,
+ });
+ }
+
+ $object = $class->new_from_hash($data);
+ $class->_object_cache_set($param, $object);
+
return $object;
}
@@ -69,7 +114,7 @@ sub new {
# Bugzilla::Object, make sure that you modify bz_setup_database
# in Bugzilla::DB::Pg appropriately, to add the right LOWER
# index. You can see examples already there.
-sub _init {
+sub _load_from_db {
my $class = shift;
my ($param) = @_;
my $dbh = Bugzilla->dbh;
@@ -82,19 +127,19 @@ sub _init {
if (ref $param eq 'HASH') {
$id = $param->{id};
}
- my $object;
+ my $object_data;
if (defined $id) {
# We special-case if somebody specifies an ID, so that we can
# validate it as numeric.
detaint_natural($id)
|| ThrowCodeError('param_must_be_numeric',
- {function => $class . '::_init'});
+ {function => $class . '::_load_from_db'});
# Too large integers make PostgreSQL crash.
return if $id > MAX_INT_32;
- $object = $dbh->selectrow_hashref(qq{
+ $object_data = $dbh->selectrow_hashref(qq{
SELECT $columns FROM $table
WHERE $id_field = ?}, undef, $id);
} else {
@@ -121,11 +166,123 @@ sub _init {
}
map { trick_taint($_) } @values;
- $object = $dbh->selectrow_hashref(
+ $object_data = $dbh->selectrow_hashref(
"SELECT $columns FROM $table WHERE $condition", undef, @values);
}
+ return $object_data;
+}
- return $object;
+sub new_from_list {
+ my $invocant = shift;
+ my $class = ref($invocant) || $invocant;
+ my ($id_list) = @_;
+ my $id_field = $class->ID_FIELD;
+
+ my @detainted_ids;
+ foreach my $id (@$id_list) {
+ detaint_natural($id) ||
+ ThrowCodeError('param_must_be_numeric',
+ {function => $class . '::new_from_list'});
+ # Too large integers make PostgreSQL crash.
+ next if $id > MAX_INT_32;
+ push(@detainted_ids, $id);
+ }
+
+ # We don't do $invocant->match because some classes have
+ # their own implementation of match which is not compatible
+ # with this one. However, match() still needs to have the right $invocant
+ # in order to do $class->DB_TABLE and so on.
+ my $list = match($invocant, { $id_field => \@detainted_ids });
+
+ # BMO: Populate the object cache with bug objects, which helps
+ # inline-history when viewing bugs with dependencies.
+ if ($class eq 'Bugzilla::Bug') {
+ foreach my $object (@$list) {
+ $class->_object_cache_set(
+ { id => $object->id, cache => 1 },
+ $object
+ );
+ }
+ }
+
+ return $list;
+}
+
+sub new_from_hash {
+ my $invocant = shift;
+ my $class = ref($invocant) || $invocant;
+ my $object_data = shift || return;
+ $class->_serialisation_keys($object_data);
+ bless($object_data, $class);
+ $object_data->initialize();
+ return $object_data;
+}
+
+sub initialize {
+ # abstract
+}
+
+# Provides a mechanism for objects to be cached in the request_cahce
+
+sub object_cache_get {
+ my ($class, $id) = @_;
+ return $class->_object_cache_get(
+ { id => $id, cache => 1},
+ $class
+ );
+}
+
+sub object_cache_set {
+ my $self = shift;
+ return $self->_object_cache_set(
+ { id => $self->id, cache => 1 },
+ $self
+ );
+}
+
+sub _object_cache_get {
+ my $class = shift;
+ my ($param) = @_;
+ my $cache_key = $class->object_cache_key($param)
+ || return;
+ return Bugzilla->request_cache->{$cache_key};
+}
+
+sub _object_cache_set {
+ my $class = shift;
+ my ($param, $object) = @_;
+ my $cache_key = $class->object_cache_key($param)
+ || return;
+ Bugzilla->request_cache->{$cache_key} = $object;
+}
+
+sub _object_cache_remove {
+ my $class = shift;
+ my ($param, $object) = @_;
+ $param->{cache} = 1;
+ my $cache_key = $class->object_cache_key($param)
+ || return;
+ delete Bugzilla->request_cache->{$cache_key};
+}
+
+sub object_cache_key {
+ my $class = shift;
+ my ($param) = @_;
+ if (ref($param) && $param->{cache} && ($param->{id} || $param->{name})) {
+ $class = blessed($class) if blessed($class);
+ return $class . ',' . ($param->{id} || $param->{name});
+ } else {
+ return;
+ }
+}
+
+# To support serialisation, we need to capture the keys in an object's default
+# hashref.
+sub _serialisation_keys {
+ my ($class, $object) = @_;
+ my $cache = Bugzilla->request_cache->{serialisation_keys} ||= {};
+ $cache->{$class} = [ keys %$object ] if $object && !exists $cache->{$class};
+ return @{ $cache->{$class} };
}
sub check {
@@ -161,28 +318,6 @@ sub check {
return $obj;
}
-sub new_from_list {
- my $invocant = shift;
- my $class = ref($invocant) || $invocant;
- my ($id_list) = @_;
- my $id_field = $class->ID_FIELD;
-
- my @detainted_ids;
- foreach my $id (@$id_list) {
- detaint_natural($id) ||
- ThrowCodeError('param_must_be_numeric',
- {function => $class . '::new_from_list'});
- # Too large integers make PostgreSQL crash.
- next if $id > MAX_INT_32;
- push(@detainted_ids, $id);
- }
- # We don't do $invocant->match because some classes have
- # their own implementation of match which is not compatible
- # with this one. However, match() still needs to have the right $invocant
- # in order to do $class->DB_TABLE and so on.
- return match($invocant, { $id_field => \@detainted_ids });
-}
-
# Note: Future extensions to this could be:
# * Add a MATCH_JOIN constant so that we can join against
# certain other tables for the WHERE criteria.
@@ -228,8 +363,11 @@ sub match {
}
next;
}
-
- $class->_check_field($field, 'match');
+
+ # It's always safe to use the field defined by classes as being
+ # their ID field. In particular, this means that new_from_list()
+ # is exempted from this check.
+ $class->_check_field($field, 'match') unless $field eq $class->ID_FIELD;
if (ref $value eq 'ARRAY') {
# IN () is invalid SQL, and if we have an empty list
@@ -263,23 +401,46 @@ sub _do_list_select {
my $cols = join(',', $class->_get_db_columns);
my $order = $class->LIST_ORDER;
- my $sql = "SELECT $cols FROM $table";
- if (defined $where) {
- $sql .= " WHERE $where ";
+ # Unconditional requests for configuration data are cacheable.
+ my ($objects, $set_memcached, $memcached_key);
+ if (!defined $where
+ && Bugzilla->memcached->enabled
+ && $class->IS_CONFIG)
+ {
+ $memcached_key = "$class:get_all";
+ $objects = Bugzilla->memcached->get_config({ key => $memcached_key });
+ $set_memcached = $objects ? 0 : 1;
}
- $sql .= " ORDER BY $order";
-
- $sql .= " $postamble" if $postamble;
-
- my $dbh = Bugzilla->dbh;
- # Sometimes the values are tainted, but we don't want to untaint them
- # for the caller. So we copy the array. It's safe to untaint because
- # they're only used in placeholders here.
- my @untainted = @{ $values || [] };
- trick_taint($_) foreach @untainted;
- my $objects = $dbh->selectall_arrayref($sql, {Slice=>{}}, @untainted);
- bless ($_, $class) foreach @$objects;
- return $objects
+
+ if (!$objects) {
+ my $sql = "SELECT $cols FROM $table";
+ if (defined $where) {
+ $sql .= " WHERE $where ";
+ }
+ $sql .= " ORDER BY $order";
+ $sql .= " $postamble" if $postamble;
+
+ my $dbh = Bugzilla->dbh;
+ # Sometimes the values are tainted, but we don't want to untaint them
+ # for the caller. So we copy the array. It's safe to untaint because
+ # they're only used in placeholders here.
+ my @untainted = @{ $values || [] };
+ trick_taint($_) foreach @untainted;
+ $objects = $dbh->selectall_arrayref($sql, {Slice=>{}}, @untainted);
+ $class->_serialisation_keys($objects->[0]) if @$objects;
+ }
+
+ if ($objects && $set_memcached) {
+ Bugzilla->memcached->set_config({
+ key => $memcached_key,
+ data => $objects
+ });
+ }
+
+ foreach my $object (@$objects) {
+ $object = $class->new_from_hash($object);
+ }
+ return $objects;
}
###############################
@@ -332,12 +493,17 @@ sub set_all {
my %field_values = %$params;
my @sorted_names = $self->_sort_by_dep(keys %field_values);
+
foreach my $key (@sorted_names) {
# It's possible for one set_ method to delete a key from $params
# for another set method, so if that's happened, we don't call the
# other set method.
next if !exists $field_values{$key};
my $method = "set_$key";
+ if (!$self->can($method)) {
+ my $class = ref($self) || $self;
+ ThrowCodeError("unknown_method", { method => "${class}::${method}" });
+ }
$self->$method($field_values{$key}, \%field_values);
}
Bugzilla::Hook::process('object_end_of_set_all',
@@ -398,6 +564,13 @@ sub update {
$self->audit_log(\%changes) if $self->AUDIT_UPDATES;
$dbh->bz_commit_transaction();
+ if ($self->USE_MEMCACHED && @values) {
+ Bugzilla->memcached->clear({ table => $table, id => $self->id });
+ Bugzilla->memcached->clear_config()
+ if $self->IS_CONFIG;
+ }
+ $self->_object_cache_remove({ id => $self->id });
+ $self->_object_cache_remove({ name => $self->name }) if $self->name;
if (wantarray) {
return (\%changes, $old_self);
@@ -416,6 +589,13 @@ sub remove_from_db {
$self->audit_log(AUDIT_REMOVE) if $self->AUDIT_REMOVES;
$dbh->do("DELETE FROM $table WHERE $id_field = ?", undef, $self->id);
$dbh->bz_commit_transaction();
+ if ($self->USE_MEMCACHED) {
+ Bugzilla->memcached->clear({ table => $table, id => $self->id });
+ Bugzilla->memcached->clear_config()
+ if $self->IS_CONFIG;
+ }
+ $self->_object_cache_remove({ id => $self->id });
+ $self->_object_cache_remove({ name => $self->name }) if $self->name;
undef $self;
}
@@ -450,6 +630,13 @@ sub audit_log {
}
}
+sub flatten_to_hash {
+ my $self = shift;
+ my $class = blessed($self);
+ my %hash = map { $_ => $self->{$_} } $class->_serialisation_keys;
+ return \%hash;
+}
+
###############################
#### Subroutines ######
###############################
@@ -473,6 +660,13 @@ sub create {
my $object = $class->insert_create_data($field_values);
$dbh->bz_commit_transaction();
+ if (Bugzilla->memcached->enabled
+ && $class->USE_MEMCACHED
+ && $class->IS_CONFIG)
+ {
+ Bugzilla->memcached->clear_config();
+ }
+
return $object;
}
@@ -568,7 +762,7 @@ sub insert_create_data {
sub get_all {
my $class = shift;
- return @{$class->_do_list_select()};
+ return @{ $class->_do_list_select() };
}
###############################
@@ -977,6 +1171,17 @@ database matching the parameters you passed in.
=back
+=item C<initialize>
+
+=over
+
+=item B<Description>
+
+Abstract method to allow subclasses to perform initialization tasks after an
+object has been created.
+
+=back
+
=item C<check>
=over
@@ -1267,6 +1472,58 @@ Returns C<1> if the passed-in value is true, C<0> otherwise.
=back
+=head2 CACHE FUNCTIONS
+
+=over
+
+=item C<object_cache_get>
+
+=over
+
+=item B<Description>
+
+Class function which returns an object from the object-cache for the provided
+C<$id>.
+
+=item B<Params>
+
+Takes an integer C<$id> of the object to retrieve.
+
+=item B<Returns>
+
+Returns the object from the cache if found, otherwise returns C<undef>.
+
+=item B<Example>
+
+my $bug_from_cache = Bugzilla::Bug->object_cache_get(35);
+
+=back
+
+=item C<object_cache_set>
+
+=over
+
+=item B<Description>
+
+Object function which injects the object into the object-cache, using the
+object's C<id> as the key.
+
+=item B<Params>
+
+(none)
+
+=item B<Returns>
+
+(nothing)
+
+=item B<Example>
+
+$bug->object_cache_set();
+
+=back
+
+=back
+
=head1 CLASS FUNCTIONS
=over