3 * Class for fetching backlink lists, approximate backlink counts and partitions.
4 * Instances of this class should typically be fetched with $title->getBacklinkCache().
6 * Ideally you should only get your backlinks from here when you think there is some
7 * advantage in caching them. Otherwise it's just a waste of memory.
10 var $partitionCache = array();
11 var $fullResultCache = array();
15 const CACHE_EXPIRY = 3600;
18 * Create a new BacklinkCache
20 function __construct( $title ) {
21 $this->title = $title;
25 * Serialization handler, diasallows to serialize the database to prevent
26 * failures after this class is deserialized from cache with dead DB connection.
29 return array( 'partitionCache', 'fullResultCache', 'title' );
33 * Clear locally stored data
36 $this->partitionCache = array();
37 $this->fullResultCache = array();
42 * Set the Database object to use
44 public function setDB( $db ) {
48 protected function getDB() {
49 if ( !isset( $this->db ) ) {
50 $this->db = wfGetDB( DB_SLAVE );
57 * Get the backlinks for a given table. Cached in process memory only.
58 * @param $table String
59 * @param $startId Integer or false
60 * @param $endId Integer or false
63 public function getLinks( $table, $startId = false, $endId = false ) {
64 wfProfileIn( __METHOD__ );
66 $fromField = $this->getPrefix( $table ) . '_from';
68 if ( $startId || $endId ) {
69 // Partial range, not cached
70 wfDebug( __METHOD__ . ": from DB (uncacheable range)\n" );
71 $conds = $this->getConditions( $table );
73 // Use the from field in the condition rather than the joined page_id,
74 // because databases are stupid and don't necessarily propagate indexes.
76 $conds[] = "$fromField >= " . intval( $startId );
80 $conds[] = "$fromField <= " . intval( $endId );
83 $res = $this->getDB()->select(
84 array( $table, 'page' ),
85 array( 'page_namespace', 'page_title', 'page_id' ),
90 'ORDER BY' => $fromField
92 $ta = TitleArray::newFromResult( $res );
94 wfProfileOut( __METHOD__ );
98 if ( !isset( $this->fullResultCache[$table] ) ) {
99 wfDebug( __METHOD__ . ": from DB\n" );
100 $res = $this->getDB()->select(
101 array( $table, 'page' ),
102 array( 'page_namespace', 'page_title', 'page_id' ),
103 $this->getConditions( $table ),
107 'ORDER BY' => $fromField,
109 $this->fullResultCache[$table] = $res;
112 $ta = TitleArray::newFromResult( $this->fullResultCache[$table] );
114 wfProfileOut( __METHOD__ );
119 * Get the field name prefix for a given table
121 protected function getPrefix( $table ) {
122 static $prefixes = array(
124 'imagelinks' => 'il',
125 'categorylinks' => 'cl',
126 'templatelinks' => 'tl',
130 if ( isset( $prefixes[$table] ) ) {
131 return $prefixes[$table];
133 throw new MWException( "Invalid table \"$table\" in " . __CLASS__ );
138 * Get the SQL condition array for selecting backlinks, with a join on the page table
140 protected function getConditions( $table ) {
141 $prefix = $this->getPrefix( $table );
145 case 'templatelinks':
148 "{$prefix}_namespace" => $this->title->getNamespace(),
149 "{$prefix}_title" => $this->title->getDBkey(),
150 "page_id={$prefix}_from"
155 'il_to' => $this->title->getDBkey(),
159 case 'categorylinks':
161 'cl_to' => $this->title->getDBkey(),
166 throw new MWException( "Invalid table \"$table\" in " . __CLASS__ );
173 * Get the approximate number of backlinks
175 public function getNumLinks( $table ) {
176 if ( isset( $this->fullResultCache[$table] ) ) {
177 return $this->fullResultCache[$table]->numRows();
180 if ( isset( $this->partitionCache[$table] ) ) {
181 $entry = reset( $this->partitionCache[$table] );
182 return $entry['numRows'];
185 $titleArray = $this->getLinks( $table );
187 return $titleArray->count();
191 * Partition the backlinks into batches.
192 * Returns an array giving the start and end of each range. The first batch has
193 * a start of false, and the last batch has an end of false.
195 * @param $table String: the links table name
196 * @param $batchSize Integer
199 public function partition( $table, $batchSize ) {
201 if ( isset( $this->partitionCache[$table][$batchSize] ) ) {
202 wfDebug( __METHOD__ . ": got from partition cache\n" );
203 return $this->partitionCache[$table][$batchSize]['batches'];
206 $this->partitionCache[$table][$batchSize] = false;
207 $cacheEntry =& $this->partitionCache[$table][$batchSize];
209 // Try full result cache
210 if ( isset( $this->fullResultCache[$table] ) ) {
211 $cacheEntry = $this->partitionResult( $this->fullResultCache[$table], $batchSize );
212 wfDebug( __METHOD__ . ": got from full result cache\n" );
214 return $cacheEntry['batches'];
220 $memcKey = wfMemcKey(
222 md5( $this->title->getPrefixedDBkey() ),
227 $memcValue = $wgMemc->get( $memcKey );
229 if ( is_array( $memcValue ) ) {
230 $cacheEntry = $memcValue;
231 wfDebug( __METHOD__ . ": got from memcached $memcKey\n" );
233 return $cacheEntry['batches'];
236 // Fetch from database
237 $this->getLinks( $table );
238 $cacheEntry = $this->partitionResult( $this->fullResultCache[$table], $batchSize );
240 $wgMemc->set( $memcKey, $cacheEntry, self::CACHE_EXPIRY );
242 wfDebug( __METHOD__ . ": got from database\n" );
243 return $cacheEntry['batches'];
247 * Partition a DB result with backlinks in it into batches
249 protected function partitionResult( $res, $batchSize ) {
251 $numRows = $res->numRows();
252 $numBatches = ceil( $numRows / $batchSize );
254 for ( $i = 0; $i < $numBatches; $i++ ) {
258 $rowNum = intval( $numRows * $i / $numBatches );
259 $res->seek( $rowNum );
260 $row = $res->fetchObject();
261 $start = $row->page_id;
264 if ( $i == $numBatches - 1 ) {
267 $rowNum = intval( $numRows * ( $i + 1 ) / $numBatches );
268 $res->seek( $rowNum );
269 $row = $res->fetchObject();
270 $end = $row->page_id - 1;
274 if ( $start && $end && $start > $end ) {
275 throw new MWException( __METHOD__ . ': Internal error: query result out of order' );
278 $batches[] = array( $start, $end );
281 return array( 'numRows' => $numRows, 'batches' => $batches );