-
-
Notifications
You must be signed in to change notification settings - Fork 2.7k
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
Changes from all commits
e43ed30
7d74667
31c545f
feda99f
3ce72d0
31396a8
3e390e5
fd07e2f
62af9ad
fc73c37
692129e
a3f8358
fa86026
04629db
ebeb044
c715652
17f681e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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); | ||
} | ||
} |
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 | ||
{ | ||
/** | ||
* 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 . "`"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why un-quote then re-quote? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In Db::getColumnNamesFromTable() we have already such logic, could it be reused? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,10 +9,8 @@ | |
namespace Piwik\Tracker; | ||
|
||
use Exception; | ||
use PDOStatement; | ||
use Piwik\Common; | ||
use Piwik\Tracker; | ||
use Piwik\Tracker\Db\DbException; | ||
|
||
class Model | ||
{ | ||
|
@@ -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(?),?,?)"; | ||
|
@@ -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; | ||
} | ||
|
@@ -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; | ||
|
@@ -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; | ||
|
@@ -375,9 +411,21 @@ private function visitFieldsToQuery($valuesToUpdate) | |
return array($updateParts, $sqlBind); | ||
} | ||
|
||
private function deleteDuplicateAction($newActionId) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Which class do you mean? (couldn't see a method called |
||
{ | ||
$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 = ? )"; | ||
} | ||
} |
There was a problem hiding this comment.
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 stuffPiwik\DataAccess\
for code related to linking domain stuff to the dbTo 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 aPiwik\Db\Schema
class (like theSchema
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…There was a problem hiding this comment.
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.