This is something that has been floating around in my personal code library for a while: a MongoDB version of pager_query(). Pretty useful :) I've implemented it on the Mongo watchdog page for starters.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

carlos8f’s picture

Status: Active » Needs review

Note that the watchdog table has no pager in the 6.x branch, and in HEAD there is this mess in mongodb_watchdog_overview():


  global $pager_page_array, $pager_total, $pager_total_items, $pager_limits;
  $per_page = 50;
  $page = isset($_GET['page']) ? $_GET['page'] : '';
  $pager_page_array = explode(',', $page);
  $on_page = $pager_page_array[0];

  $cursor = mongodb_collection(variable_get('mongodb_collectionname', 'watchdog'))
    ->find(mongodb_watchdog_build_filter_query())
    ->limit($per_page)
    ->skip($on_page * $per_page)
    ->sort(array('timestamp' => -1));

Could be cleaned up nicely with this helper. Will port to D7 as well.

carlos8f’s picture

Version: 6.x-1.x-dev » 7.x-1.x-dev
FileSize
4.33 KB

Ported to HEAD.

carlos8f’s picture

mongodb_watchdog_pager_init() can be removed, too.

carlos8f’s picture

Status: Needs review » Needs work

Whoops, I forgot to implement $sort.

thebuckst0p’s picture

An alternative approach which I'm using involves setting the limit() and skip() on the query separately, then using the query cursor to set the global pager variables, like so:

/**
 * set the global pager variables from a mongo query cursor,
 * so theme('pager') works
 *
 * DOES NOT run a query or set a limit - has to be set separately on the cursor/query
 *  w/ $cursor->skip($page * $per_page)->limit($per_page)
 * then this function checks the limit() w/ count(TRUE)
 * (see http://www.php.net/manual/en/mongocursor.count.php)
 *
 * @param $cursor a mongodb cursor, already queried and limit()'d
 * @param $element An optional integer to distinguish between multiple pagers on one page. (as in pager_query())
 *
 * @return nothing [sets global variables]
 */
function mongodb_set_pager($cursor, $element = 0) {
  global $pager_page_array, $pager_total, $pager_total_items;
  $pager_page_array = explode(',', $page);
  $pager_element = 0;
  $pager_total_items[$pager_element] = $cursor->count(FALSE);   // total w/o limit
  $per_page = $cursor->count(TRUE);   // w/ limit
  $pager_total[$pager_element] = ceil($pager_total_items[$pager_element] / $per_page);
  $pager_page_array[$pager_element] = max(0, min((int) $pager_page_array[$pager_element], ((int) $pager_total[$pager_element]) - 1));  
}
thebuckst0p’s picture

Coming back to this after a while - my function was missing a critical line for $page. Corrected:

function mongodb_set_pager($cursor, $element = 0) {
  global $pager_page_array, $pager_total, $pager_total_items;
  $page = isset($_GET['page']) ? $_GET['page'] : '';

  $pager_page_array = explode(',', $page);
  $pager_element = 0;
  $pager_total_items[$pager_element] = $cursor->count(FALSE);   // total w/o limit
  $per_page = $cursor->count(TRUE);   // w/ limit
  $pager_total[$pager_element] = ceil($pager_total_items[$pager_element] / $per_page);
  $pager_page_array[$pager_element] = max(0, min((int) $pager_page_array[$pager_element], ((int) $pager_total[$pager_element]) - 1));
}
Alexander Matveev’s picture

How to use this in some custom module?
I'm using some about code written below: I see pager and docs list, but the list is always the same, there is no reaction to pager surfing..

try {
    $m = new MongoClient('mongodb://localhost/');
    $docs = $m->xxx->publisher_sites->find(array('uid' => (int)$user->uid))->limit(10)->sort(array('_id' => -1));
    mongodb_set_pager($docs);
    $build['items']['#markup'] = '';
    foreach($docs as $doc) {
      $id = end($doc['_id']);
      $build['items']['#markup'] .= $id . '<br/>';
    }
    $build['pager'] = array(
      '#theme' => 'pager',
    );
    return $build;
  } catch ( MongoCursorException $e ) {
    drupal_set_message($e->getCode() . ': ' . $e->getMessage(), 'error');
  }
  $m->close();
khayong’s picture

I'm using object oriented way to handle paging. It is just a wrapper class to a collection, just like the PagerDefault class in drupal. User just have to call single line of code as below

$collection = new mongoPagerCollection($collection, 50);

class mongoPagerCollection {
  public function __construct($collection, $limit = 50, $element = 0) {
    $this->collection = $collection;
    $this->limit = $limit;
    $this->element = $element;
  }

  function find($query = array(), $fields = array()) {
    $total_items = $this->collection->count($query);
    $this->current_page = pager_default_initialize($total_items, $this->limit, $this->element);

    $cursor = $this->collection->find($query, $fields);
    $cursor->skip($this->current_page * $this->limit);
    $cursor->limit($this->limit);

    return $cursor;
  }

  function getCurrentPage() {
    return $this->current_page;
  }

  function getLimit() {
    return $this->limit;
  }

  function __call($name, $arguments) {
    return call_user_func_array(array($this->collection, $name), $arguments);
  }
}
khayong’s picture

I also created a sorting class like TableSort, to handle header sorting in the table. User just have to call

$collection = new mongoTableSortCollection($collection, $header);

Here is the code of the class

class mongoTableSortCollection {
  public function __construct($collection, $header) {
    $this->collection = $collection;
    $this->header = $header;
  }

  function find($query = array(), $fields = array()) {
    $cursor = $this->collection->find($query, $fields);

    $ts = tablesort_get_order($this->header);
    $ts['sort'] = tablesort_get_sort($this->header);
    $ts['query'] = tablesort_get_query_parameters();

    if (!empty($ts['sql'])) {
      // Based on code from db_escape_table(), but this can also contain a dot.
      $field = preg_replace('/[^A-Za-z0-9_.]+/', '', $ts['sql']);

      // Sort order can only be ASC or DESC.
      $sort = drupal_strtoupper($ts['sort']);
      $sort = in_array($sort, array('ASC', 'DESC')) ? $sort : '';
      $cursor->sort(array($field => (empty($sort) || $sort == 'ASC' ? 1 : -1)));
    }

    return $cursor;
  }

  function __call($name, $arguments) {
    return call_user_func_array(array($this->collection, $name), $arguments);
  }
}

Note that if you want to have both pager and sorting together, you will need to wrapped in mongoTableSortCollection first, then only wrapped in mongoPagerCollection. Otherwise, the speed will be a bit slow. Here are the sample of the code

$collection = new mongoTableSortCollection($collection, $header);
$collection = new mongoPagerCollection($collection, 50);
mcabalaji’s picture

Issue summary: View changes

Tried the same mentioned in #1005790-9: Mongo alternative to pager_query() and does not seem to work.

The pager_total variable is getting cleared before pager.inc. There by making pager not showing up in the page.

Do let me know anything I have to add apart from the mentioned item.

fgm’s picture

Just a passing note : using skip() is not a good practice for pagers. In most cases, it will force the DB server to build the list of documents, browse it until the skip count, and return the rest.

What you want instead is order on something indexed and query using an expected value on this indexed sort : that way the system needn't browse from the start of the matching rows. This is explained in other words on http://docs.mongodb.org/manual/reference/method/cursor.skip/

Rizwan Siddiquee’s picture

global $pager_page_array, $pager_total, $pager_total_items, $pager_limits;
$per_page = 10;
$page = isset($_GET['page']) ? $_GET['page'] : '';
$pager_page_array = explode(',', $page);
$on_page = $pager_page_array[0];

// result query
$result1 = $res->find(array("exam_nid" => (int)$exam_id))->limit($per_page)->skip($on_page * $per_page);

$form['path']['table'] = array(
'#theme' => 'table',
'#header' => $header,
'#rows' => $row,
'#empty' => t('Table has no row!')
);

// Add the pager.
if ($on_page > 0 || count($row) >= $per_page) {
$pager_total_items[0] = $result1->count();
$pager_total[0] = ceil($pager_total_items[0] / $per_page);
$pager_page_array[0] = max(0, min((int) $pager_page_array[0], ((int)$pager_total[0]) - 1));
$pager_limits[0] = $per_page;
$form['path']['pager'] = array(
'#theme' => 'pager',
);
}

fgm’s picture

Rerolled the initial patch by carlos8f on today's HEAD.

carlos8f’s picture

@fgm LOL you're STILL HERE? I quit Drupal so many years ago. Good Riddance. I use Node.js now, it's so clean, fast, and far superior in every way to PHP. For realz. https://s8f.org/

carlos8f’s picture

i helped build Drupal 6/7 with @chx, @webchick, @catch, @drothstein, @sun, @dries, @Damien Tournoud, @Dave Reid, @Fabianx, etc.

That was fun, I made some friends with real-life hackers at DrupalCon SF (a long time ago...). PHP sucks though. Seriously. Use Node.js or Python.

fgm’s picture

@carlos8f : doing a lot of Node in the last 2 years and hated every minute of it. :troll: I very much prefer working with Go or PHP, but YMMV.

Anyway, rerolled the #6 patch by thebuckst0p on current 7.x-1.x HEAD.

fgm’s picture

Rerolled the #9 patch by khayong on top of today's HEAD.

For ease of use, this issue is available as a branch on the github repo as a pull request: https://github.com/fgm/mongodb/pull/32

Since the patches are all different and not compatible, each of them is reverted after being committed, so be sure to checkout the commit matching what you want to use, and comment here so one version can actually be chosen.