Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor new action insertion code so duplicate actions will not exist + provide means to fix duplicates #7112

Merged
merged 17 commits into from
Feb 9, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions core/DataAccess/Actions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
namespace Piwik\DataAccess;

use Piwik\Db;
use Piwik\Common;

/**
* Data Access Object for operations dealing with the log_action table.
*/
class Actions
{
/**
* Removes a list of actions from the log_action table by ID.
*
* @param int[] $idActions
*/
public function delete($idActions)
{
foreach ($idActions as &$id) {
$id = (int)$id;
}

$table = Common::prefixTable('log_action');

$sql = "DELETE FROM $table WHERE idaction IN (" . implode(",", $idActions) . ")";
Db::query($sql);
}
}
56 changes: 56 additions & 0 deletions core/DataAccess/TableMetadata.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
namespace Piwik\DataAccess;

use Piwik\Db;

/**
* Data Access Object that can be used to get metadata information about
* the MySQL tables Piwik uses.
*/
class TableMetadata
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm probably picking but I believe this kind of code would be better in Piwik\Db\, i.e.

  • Piwik\Db\ for code related to database, unrelated to domain stuff
  • Piwik\DataAccess\ for code related to linking domain stuff to the db

To me getting the columns of a table is really DBAL stuff. This class could still be separated from the Db class (I see you deprecated the method), maybe in a Piwik\Db\Schema class (like the Schema class in Doctrine)? I imagine later we could have a full abstraction of schema manipulation in there (again, like in Doctrine) to get/create tables, columns, etc…

Thoughts?

Edit: I just notice now getIdActionColumnNames() so I don't know…

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, my thoughts are that getColumns is pretty generic, but getIdActionColumnNames is specific to certain use-cases/functionality. Ideally, I think all relational stuff would be in its own plugin (ie, plugins/MySQL or plugins/Relational, etc.). This way it wouldn't need to be in core, but Piwik is pretty far away from the possibility of defining a data access layer in a plugin.

FYI, I only moved it in response to @tsteur last comment about making as much re-usable as possible.

{
/**
* Returns the list of column names for a table.
*
* @param string $table Prefixed table name.
* @return string[] List of column names..
*/
public function getColumns($table)
{
$table = str_replace("`", "", $table);

$columns = Db::fetchAll("SHOW COLUMNS FROM `" . $table . "`");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why un-quote then re-quote?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm attempting to avoid SQL injections here (since getColumns is a public method, I am assuming its possible for plugin developers to do stupid things w/ it).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In Db::getColumnNamesFromTable() we have already such logic, could it be reused?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mattab the code was moved from that method (see here)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok nice!


$columnNames = array();
foreach ($columns as $column) {
$columnNames[] = $column['Field'];
}

return $columnNames;
}

/**
* Returns the list of idaction columns in a table. A column is
* assumed to be an idaction reference if it has `"idaction"` in its
* name (eg, `"idaction_url"` or `"idaction_content_name"`.
*
* @param string $table Prefixed table name.
* @return string[]
*/
public function getIdActionColumnNames($table)
{
$columns = $this->getColumns($table);

$columns = array_filter($columns, function ($columnName) {
return strpos($columnName, 'idaction') !== false;
});

return array_values($columns);
}
}
12 changes: 4 additions & 8 deletions core/Db.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
namespace Piwik;

use Exception;
use Piwik\DataAccess\TableMetadata;
use Piwik\Db\Adapter;
use Piwik\Tracker;

Expand Down Expand Up @@ -387,17 +388,12 @@ public static function dropAllTables()
*
* @param string|array $table The name of the table you want to get the columns definition for.
* @return \Zend_Db_Statement
* @deprecated since 2.11.0
*/
public static function getColumnNamesFromTable($table)
{
$columns = self::fetchAll("SHOW COLUMNS FROM `" . $table . "`");

$columnNames = array();
foreach ($columns as $column) {
$columnNames[] = $column['Field'];
}

return $columnNames;
$tableMetadataAccess = new TableMetadata();
return $tableMetadataAccess->getColumns($table);
}

/**
Expand Down
58 changes: 53 additions & 5 deletions core/Tracker/Model.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,8 @@
namespace Piwik\Tracker;

use Exception;
use PDOStatement;
use Piwik\Common;
use Piwik\Tracker;
use Piwik\Tracker\Db\DbException;

class Model
{
Expand Down Expand Up @@ -142,7 +140,31 @@ public function createEcommerceItems($ecommerceItems)
Common::printDebug($bind);
}

/**
* Inserts a new action into the log_action table. If there is an existing action that was inserted
* due to another request pre-empting this one, the newly inserted action is deleted.
*
* @param string $name
* @param int $type
* @param int $urlPrefix
* @return int The ID of the action (can be for an existing action or new action).
*/
public function createNewIdAction($name, $type, $urlPrefix)
{
$newActionId = $this->insertNewAction($name, $type, $urlPrefix);

$realFirstActionId = $this->getIdActionMatchingNameAndType($name, $type);

// if the inserted action ID is not the same as the queried action ID, then that means we inserted
// a duplicate, so remove it now
if ($realFirstActionId != $newActionId) {
$this->deleteDuplicateAction($newActionId);
}

return $realFirstActionId;
}

private function insertNewAction($name, $type, $urlPrefix)
{
$table = Common::prefixTable('log_action');
$sql = "INSERT INTO $table (name, hash, type, url_prefix) VALUES (?,CRC32(?),?,?)";
Expand All @@ -157,8 +179,11 @@ public function createNewIdAction($name, $type, $urlPrefix)

private function getSqlSelectActionId()
{
// it is possible for multiple actions to exist in the DB (due to rare concurrency issues), so the ORDER BY and
// LIMIT are important
$sql = "SELECT idaction, type, name FROM " . Common::prefixTable('log_action')
. " WHERE ( hash = CRC32(?) AND name = ? AND type = ? ) ";
. " WHERE " . $this->getSqlConditionToMatchSingleAction() . " "
. "ORDER BY idaction ASC LIMIT 1";

return $sql;
}
Expand All @@ -173,9 +198,16 @@ public function getIdActionMatchingNameAndType($name, $type)
return $idAction;
}

/**
* Returns the IDs for multiple actions based on name + type values.
*
* @param array $actionsNameAndType Array like `array( array('name' => '...', 'type' => 1), ... )`
* @return array|false Array of DB rows w/ columns: **idaction**, **type**, **name**.
*/
public function getIdsAction($actionsNameAndType)
{
$sql = $this->getSqlSelectActionId();
$sql = "SELECT MIN(idaction) as idaction, type, name FROM " . Common::prefixTable('log_action')
. " WHERE";
$bind = array();

$i = 0;
Expand All @@ -187,15 +219,19 @@ public function getIdsAction($actionsNameAndType)
}

if ($i > 0) {
$sql .= " OR ( hash = CRC32(?) AND name = ? AND type = ? ) ";
$sql .= " OR";
}

$sql .= " " . $this->getSqlConditionToMatchSingleAction() . " ";

$bind[] = $name;
$bind[] = $name;
$bind[] = $actionNameType['type'];
$i++;
}

$sql .= " GROUP BY type, hash, name";

// Case URL & Title are empty
if (empty($bind)) {
return false;
Expand Down Expand Up @@ -375,9 +411,21 @@ private function visitFieldsToQuery($valuesToUpdate)
return array($updateParts, $sqlBind);
}

private function deleteDuplicateAction($newActionId)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deleteDuplicateActions?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only deletes one action, but I think this is a redundancy w/ Actions::deleteDuplicateActions. Anyone see a problem w/ using that class from within Tracker/Model?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which class do you mean? (couldn't see a method called deleteDuplicateActions)

{
$sql = "DELETE FROM " . Common::prefixTable('log_action') . " WHERE idaction = ?";

$db = $this->getDb();
$db->query($sql, array($newActionId));
}

private function getDb()
{
return Tracker::getDatabase();
}

private function getSqlConditionToMatchSingleAction()
{
return "( hash = CRC32(?) AND name = ? AND type = ? )";
}
}
5 changes: 4 additions & 1 deletion core/Tracker/TableLogAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,10 @@ private static function getSelectQueryWhereNameContains($matchType, $actionType)
{
// now, we handle the cases =@ (contains) and !@ (does not contain)
// build the expression based on the match type
$sql = 'SELECT idaction FROM ' . Common::prefixTable('log_action') . ' WHERE %s AND type = ' . $actionType . ' )';
$sql = 'SELECT MIN(idaction) AS idaction FROM ' . Common::prefixTable('log_action') . ' WHERE %s AND type = ' . $actionType . ' )'
. ' GROUP BY name, hash, type'; // group by is to avoid possible case of duplicates in log_action table
// (duplicates can exist if php tracker fails right after inserting a duplicate in
// Tracker\Model::insertNewAction())

switch ($matchType) {
case '=@':
Expand Down
1 change: 0 additions & 1 deletion core/Updates/2.11.0-b2.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/

namespace Piwik\Updates;

use Piwik\Common;
Expand Down
Loading