(dev/core#217) Query::getCachedContacts - Use swappable fetch() instead of SQL JOIN
authorTim Otten <totten@civicrm.org>
Tue, 14 Aug 2018 22:12:57 +0000 (15:12 -0700)
committerTim Otten <totten@civicrm.org>
Tue, 27 Nov 2018 23:16:22 +0000 (15:16 -0800)
The general context of this code is roughly as follows:

* We've already filled up the prevnext cache with a bunch of contact-IDs.
* The user wants to view a page of 50 contacts.
* We want to lookup full information about 50 specific contacts for this page.

It does makes sense to use `CRM_Contact_BAO_Query` for looking up the "full information"
about contacts. However, the function `Query::getCachedContacts()` is hard-coded to
read from the SQL-based prevnext cache.

Before
------

* In `getCachedContacts()`, it grabbed the full SQL for `CRM_Contact_BAO_Query`
  and munged the query to:
    * Add an extra JOIN on `civicrm_prevnext_cache` (with a constraint on `cacheKey`)
    * Respect pagination (LIMIT/OFFSET)
    * Order results based on their position in the prevnext cache

After
-----

* In `CRM_Core_PrevNextCache_Interface`, the `fetch()` function provides one page-worth
  of contact IDs (in order). The `fetch()` function is tested by `E2E_Core_PrevNextTest`.
* In `getCachedContacts()`, it doesn't know anything about `civicrm_prevnext_cache`
  or `cacheKey` or pagination.  Instead, it just accepts CIDs for one page-worth of
  contacts.  It returns contacts in the same order that was given.

CRM/Contact/BAO/Query.php
CRM/Contact/Selector.php
CRM/Core/PrevNextCache/Interface.php
CRM/Core/PrevNextCache/Sql.php
tests/phpunit/E2E/Core/PrevNextTest.php

index e118fd7fc681c2ded2cf91bb3337ec40adfab06c..71497d2e24904e3674d6620e6a228ca91fbf2a48 100644 (file)
@@ -4949,29 +4949,47 @@ civicrm_relationship.start_date > {$today}
   }
 
   /**
-   * Fetch a list of contacts from the prev/next cache for displaying a search results page
+   * Fetch a list of contacts for displaying a search results page
    *
-   * @param string $cacheKey
-   * @param int $offset
-   * @param int $rowCount
+   * @param array $cids
+   *   List of contact IDs
    * @param bool $includeContactIds
    * @return CRM_Core_DAO
    */
-  public function getCachedContacts($cacheKey, $offset, $rowCount, $includeContactIds) {
+  public function getCachedContacts($cids, $includeContactIds) {
+    CRM_Utils_Type::validateAll($cids, 'Positive');
     $this->_includeContactIds = $includeContactIds;
     $onlyDeleted = in_array(array('deleted_contacts', '=', '1', '0', '0'), $this->_params);
     list($select, $from, $where) = $this->query(FALSE, FALSE, FALSE, $onlyDeleted);
-    $from = " FROM civicrm_prevnext_cache pnc INNER JOIN civicrm_contact contact_a ON contact_a.id = pnc.entity_id1 AND pnc.cacheKey = '$cacheKey' " . substr($from, 31);
-    $order = " ORDER BY pnc.id";
-    $groupByCol = array('contact_a.id', 'pnc.id');
-    $select = self::appendAnyValueToSelect($this->_select, $groupByCol, 'GROUP_CONCAT');
-    $groupBy = " GROUP BY " . implode(', ', $groupByCol);
-    $limit = " LIMIT $offset, $rowCount";
+    $select .= sprintf(", (%s) AS _wgt", $this->createSqlCase('contact_a.id', $cids));
+    $where .= sprintf(' AND contact_a.id IN (%s)', implode(',', $cids));
+    $order = 'ORDER BY _wgt';
+    $groupBy = '';
+    $limit = '';
     $query = "$select $from $where $groupBy $order $limit";
 
     return CRM_Core_DAO::executeQuery($query);
   }
 
+  /**
+   * Construct a SQL CASE expression.
+   *
+   * @param string $idCol
+   *   The name of a column with ID's (eg 'contact_a.id').
+   * @param array $cids
+   *   Array(int $weight => int $id).
+   * @return string
+   *   CASE WHEN id=123 THEN 1 WHEN id=456 THEN 2 END
+   */
+  private function createSqlCase($idCol, $cids) {
+    $buf = "CASE\n";
+    foreach ($cids as $weight => $cid) {
+      $buf .= " WHEN $idCol = $cid THEN $weight \n";
+    }
+    $buf .= "END\n";
+    return $buf;
+  }
+
   /**
    * Populate $this->_permissionWhereClause with permission related clause and update other
    * query related properties.
index f048612ab7116eb803f6c86ded746d321315ddd2..f60d434fbdf315790c8fa71dc10610422411a08f 100644 (file)
@@ -578,8 +578,11 @@ class CRM_Contact_Selector extends CRM_Core_Selector_Base implements CRM_Core_Se
     // and contain the search criteria (parameters)
     // note that the default action is basic
     if ($rowCount) {
+      /** @var CRM_Core_PrevNextCache_Interface $prevNext */
+      $prevNext = Civi::service('prevnext');
       $cacheKey = $this->buildPrevNextCache($sort);
-      $resultSet = $this->_query->getCachedContacts($cacheKey, $offset, $rowCount, $includeContactIds)->fetchGenerator();
+      $cids = $prevNext->fetch($cacheKey, $offset, $rowCount);
+      $resultSet = empty($cids) ? [] : $this->_query->getCachedContacts($cids, $includeContactIds)->fetchGenerator();
     }
     else {
       $resultSet = $this->_query->searchQuery($offset, $rowCount, $sort, FALSE, $includeContactIds)->fetchGenerator();
index edaf0d6778ce9bae1658773d6d249c458f1b7a4c..5f3bfabd1e136982d99a012af3e3fc2c2eb9ecb7 100644 (file)
@@ -114,4 +114,15 @@ interface CRM_Core_PrevNextCache_Interface {
    */
   public function getCount($cacheKey);
 
+  /**
+   * Fetch a list of contacts from the prev/next cache for displaying a search results page
+   *
+   * @param string $cacheKey
+   * @param int $offset
+   * @param int $rowCount
+   * @return array
+   *   List of contact IDs (entity_id1).
+   */
+  public function fetch($cacheKey, $offset, $rowCount);
+
 }
index 5a5897353332e0ee6b74fe2d7dcd08d152271beb..fc73de4408bf3d9e0babdcde3e5a18e039d4dba3 100644 (file)
@@ -244,4 +244,27 @@ ORDER BY id
     return (int) CRM_Core_DAO::singleValueQuery($query, $params, TRUE, FALSE);
   }
 
+  /**
+   * Fetch a list of contacts from the prev/next cache for displaying a search results page
+   *
+   * @param string $cacheKey
+   * @param int $offset
+   * @param int $rowCount
+   * @return array
+   *   List of contact IDs.
+   */
+  public function fetch($cacheKey, $offset, $rowCount) {
+    $cids = array();
+    $dao = CRM_Utils_SQL_Select::from('civicrm_prevnext_cache pnc')
+      ->where('pnc.cacheKey = @cacheKey', ['cacheKey' => $cacheKey])
+      ->select('pnc.entity_id1 as cid')
+      ->orderBy('pnc.id')
+      ->limit($rowCount, $offset)
+      ->execute();
+    while ($dao->fetch()) {
+      $cids[] = $dao->cid;
+    }
+    return $cids;
+  }
+
 }
index 3ca3c48aa68b516ecc408f0b9ed7e314995977d3..d416e564b8141e49b34e9c21991cb0990b1f2f7d 100644 (file)
@@ -93,6 +93,19 @@ class PrevNextTest extends \CiviEndToEndTestCase {
     $this->assertSelections([]);
   }
 
+  public function testFetch() {
+    $this->testFillArray();
+
+    $cids = $this->prevNext->fetch($this->cacheKey, 0, 2);
+    $this->assertEquals([100, 400], $cids);
+
+    $cids = $this->prevNext->fetch($this->cacheKey, 0, 4);
+    $this->assertEquals([100, 400, 200, 300], $cids);
+
+    $cids = $this->prevNext->fetch($this->cacheKey, 2, 2);
+    $this->assertEquals([200, 300], $cids);
+  }
+
   public function getFillFunctions() {
     return [
       ['testFillSql'],