* Category objects are immutable, strictly speaking. If you call methods that change the database,
* like to refresh link counts, the objects will be appropriately reinitialized.
* Member variables are lazy-initialized.
- *
- * @todo Move some stuff from CategoryPage.php to here, and use that.
*/
class Category {
/** Name of the category, normalized to DB-key form */
const LOAD_ONLY = 0;
const LAZY_INIT_ROW = 1;
+ const ROW_COUNT_SMALL = 100;
+
private function __construct() {
}
* fields are null, the resulting Category object will represent an empty
* category if a title object was given. If the fields are null and no
* title was given, this method fails and returns false.
- * @param Title $title Optional title object for the category represented by
+ * @param Title|null $title Optional title object for the category represented by
* the given row. May be provided if it is already known, to avoid having
* to re-create a title object later.
* @return Category|false
/**
* Generic accessor
* @param string $key
- * @return bool
+ * @return mixed
*/
private function getX( $key ) {
- if ( !$this->initialize( self::LAZY_INIT_ROW ) ) {
+ if ( $this->{$key} === null && !$this->initialize( self::LAZY_INIT_ROW ) ) {
return false;
}
return $this->{$key};
// Lock the `category` row before locking `categorylinks` rows to try
// to avoid deadlocks with LinksDeletionUpdate (T195397)
- $dbw->selectField(
- 'category',
- 1,
- [ 'cat_title' => $this->mName ],
- __METHOD__,
- [ 'FOR UPDATE' ]
- );
+ $dbw->lockForUpdate( 'category', [ 'cat_title' => $this->mName ], __METHOD__ );
// Lock all the `categorylinks` records and gaps for this category;
- // this is a separate query due to postgres/oracle limitations
+ // this is a separate query due to postgres limitations
$dbw->selectRowCount(
[ 'categorylinks', 'page' ],
'*',
return true;
}
+
+ /**
+ * Call refreshCounts() if there are no entries in the categorylinks table
+ * or if the category table has a row that states that there are no entries
+ *
+ * Due to lock errors or other failures, the precomputed counts can get out of sync,
+ * making it hard to know when to delete the category row without checking the
+ * categorylinks table.
+ *
+ * @return bool Whether links were refreshed
+ * @since 1.32
+ */
+ public function refreshCountsIfEmpty() {
+ return $this->refreshCountsIfSmall( 0 );
+ }
+
+ /**
+ * Call refreshCounts() if there are few entries in the categorylinks table
+ *
+ * Due to lock errors or other failures, the precomputed counts can get out of sync,
+ * making it hard to know when to delete the category row without checking the
+ * categorylinks table.
+ *
+ * This method will do a non-locking select first to reduce contention.
+ *
+ * @param int $maxSize Only refresh if there are this or less many backlinks
+ * @return bool Whether links were refreshed
+ * @since 1.34
+ */
+ public function refreshCountsIfSmall( $maxSize = self::ROW_COUNT_SMALL ) {
+ $dbw = wfGetDB( DB_MASTER );
+ $dbw->startAtomic( __METHOD__ );
+
+ $typeOccurances = $dbw->selectFieldValues(
+ 'categorylinks',
+ 'cl_type',
+ [ 'cl_to' => $this->getName() ],
+ __METHOD__,
+ [ 'LIMIT' => $maxSize + 1 ]
+ );
+
+ if ( !$typeOccurances ) {
+ $doRefresh = true; // delete any category table entry
+ } elseif ( count( $typeOccurances ) <= $maxSize ) {
+ $countByType = array_count_values( $typeOccurances );
+ $doRefresh = !$dbw->selectField(
+ 'category',
+ '1',
+ [
+ 'cat_title' => $this->getName(),
+ 'cat_pages' => $countByType['page'] ?? 0,
+ 'cat_subcats' => $countByType['subcat'] ?? 0,
+ 'cat_files' => $countByType['file'] ?? 0
+ ],
+ __METHOD__
+ );
+ } else {
+ $doRefresh = false; // category is too big
+ }
+
+ $dbw->endAtomic( __METHOD__ );
+
+ if ( $doRefresh ) {
+ $this->refreshCounts(); // update the row
+
+ return true;
+ }
+
+ return false;
+ }
}