Description
Preconditions and environment
- 2.4.5-p5 (Applicable for all 2.4.5 versions)
The type of the value retrieved from the database is always a string, even if the original value in the database is of type decimal/float. This results in incorrect detection of data changes, triggering unnecessary updates in the save function.
Steps to reproduce
Here are the step-by-step reproduction steps:
Step 1: Add a product to the cart
- Go to the website and add a product to the cart.
Step 2: Confirm entry creation and check tax values
- Confirm that an entry is created in the
quote
andquote_item
tables. - Check the values for
tax_amount
in both thequote
andquote_item
tables.
Step 3: Add breakpoints to specific functions
- Open the following files:
a.vendor/magento/framework/Model/ResourceModel/Db/AbstractDb.php
- Locate thesave
function and add a breakpoint.
b.vendor/magento/framework/Model/AbstractModel.php
- Find thesetData
function and add a breakpoint.
Step 4: Proceed to the cart page
- Navigate to the cart page.
Step 5: Trace requests and set breakpoints
- Observe requests related to
quote_item
orquote
tax_amount during thesetData
function call. - Set breakpoints around the
collectTotals
event.
Step 6: Compare DB and incoming request values
- Intercept the request in question.
- Compare the values in the database with the incoming request.
- Pay attention to the
setData
function invendor/magento/framework/Model/AbstractModel.php
.
Step 7: Understand the data difference condition
- In the
setData
function, look for the condition:if (!array_key_exists($key, $this->_data) || $this->_data[$key] !== $value) {
- Understand that this condition considers data to be different, especially when dealing with
tax_amount
where the DB value is a string and the incoming request value is a float.
Step 8: Execute the condition leading to changes
- Realize that the condition in step 7 sets
_hasDataChanges
to true. - This triggers the logic in the
save
function ofvendor/magento/framework/Model/ResourceModel/Db/AbstractDb.php
.
Step 9: Save logic not executed
- Observe that the save logic does not go into the condition:
if (!$this->isModified($object)) { $this->processNotModifiedSave($object); $this->commit(); $object->setHasDataChanges(false); return $this; }
- This indicates that the save logic to avoid duplicate update is skipped due to the
_hasDataChanges
being set to true in the previous steps.
Expected result
A duplicate update call to DB should not be triggered if the data has not changed for the following function:
vendor/magento/framework/Model/ResourceModel/Db/AbstractDb.php
/**
* Save object object data
*
* @param \Magento\Framework\Model\AbstractModel $object
* @return $this
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
* @throws \Exception
* @throws AlreadyExistsException
*/
public function save(\Magento\Framework\Model\AbstractModel $object)
{
if ($object->isDeleted()) {
return $this->delete($object);
}
$this->beginTransaction();
try {
if (!$this->isModified($object)) {
$this->processNotModifiedSave($object);
$this->commit();
$object->setHasDataChanges(false);
return $this;
}
if (!$this->isModified($object)) {
should return true and the if condition logic shall be executed.
Result: No DB Update query should have run.
Actual result
if (!$this->isModified($object)) {
returns false and MySQL update query is ran. Which should not be the case.
Additional information
This issue affects the accuracy of detecting data changes, resulting in unnecessary database updates, impacting performance and potentially causing data integrity concerns.
investigated the save( function in depth and how Magento check if the object data has changed or not.
vendor/magento/framework/Model/ResourceModel/Db/AbstractDb.php
/**
* Save object object data
*
* @param \Magento\Framework\Model\AbstractModel $object
* @return $this
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
* @throws \Exception
* @throws AlreadyExistsException
*/
public function save(\Magento\Framework\Model\AbstractModel $object)
{
if ($object->isDeleted()) {
return $this->delete($object);
}
$this->beginTransaction();
try {
if (!$this->isModified($object)) {
$this->processNotModifiedSave($object);
$this->commit();
$object->setHasDataChanges(false);
return $this;
}
if (!$this->isModified($object)) {
this part is crucial has this further calls the following:
This checks for the following function:
vendor/magento/framework/Model/AbstractModel.php
/**
* Check if initial object data was changed.
*
* Initial data is coming to object constructor.
* Flag value should be set up to true after any external data changes
*
* @return bool
*/
public function hasDataChanges()
{
return $this->_hasDataChanges;
}
The value _hasDataChanges is set in the following function:
/**
* Overwrite data in the object.
*
* The $key parameter can be string or array.
* If $key is string, the attribute value will be overwritten by $value
*
* If $key is an array, it will overwrite all the data in the object.
*
* @param string|array $key
* @param mixed $value
* @return $this
*/
public function setData($key, $value = null)
{
if ($key === (array)$key) {
if ($this->_data !== $key) {
$this->_hasDataChanges = true;
}
$this->_data = $key;
} else {
if (!array_key_exists($key, $this->_data) || $this->_data[$key] !== $value) {
$keyValue = $this->_data[$key] ?? '';
$valueData = $value ;
$this->_hasDataChanges = true;
}
$this->_data[$key] = $value;
}
return $this;
}
Looking closely at this line of code:
if (!array_key_exists($key, $this->_data) || $this->_data[$key] !== $value) {
- The following screenshot shows how the existing quote item tax amount saved in DB is
0
. - But when another save with
0
type - float tax amount value is executed the data loaded from DB for that object has tax value as0.0000
type - string. - Therefore they do not match in their type that is the reason the value
_hasDataChanges
is set astrue
. - And this happens for every save, causing multiple duplicate MySQL updates.
The type of value retrieved from DB will always be of type string. Even if the value set in the DB column is of type decimal/float.
Suppose we call -> setTaxAmount (2.99), setting to a float. Current tax amount is already 2.99 but it's returned via getData as string "2.9900" so Magento thinks data has changed, even though it's the same value.
This causes the following save function to always execute an update query to DB even though values have not changed actually:
public function save(\Magento\Framework\Model\AbstractModel $object)
Release note
No response
Triage and priority
- Severity: S0 - Affects critical data or functionality and leaves users without workaround.
- Severity: S1 - Affects critical data or functionality and forces users to employ a workaround.
- Severity: S2 - Affects non-critical data or functionality and forces users to employ a workaround.
- Severity: S3 - Affects non-critical data or functionality and does not force users to employ a workaround.
- Severity: S4 - Affects aesthetics, professional look and feel, “quality” or “usability”.