Database transactions in Zend Framework: Are they isolated? - php

Using Zend Framework, I need to (1) read a record from a MySQL database, and (2) immediately write back to that record to indicate that it has been read. I don't want other processes or queries to be able to read from or write to the same record in between steps (1) and (2).
I was considering using a transaction for these steps. If I use the following methods, will that fulfil my requirements?
Zend_Db_Adapter_Abstract::beginTransaction()
Zend_Db_Adapter_Abstract::commit()
Zend_Db_Adapter_Abstract::rollBack()

Presupposing you are using the InnoDB engine for tables that you will issue transactions on:
If the requirement is that you first need to read the row and exclusively lock it, before you are going to update it, you should issue a SELECT ... FOR UPDATE query. Something like:
$db->beginTransaction();
try
{
$select = $db->select()
->forUpdate() // <-- here's the magic
->from(
array( 'a' => 'yourTable' ),
array( 'your', 'column', 'names' )
)
->where( 'someColumn = ?', $whatever );
$result = $this->_adapter->fetchRow( $select );
/*
alter data in $result
and update if necessary:
*/
$db->update( 'yourTable', $result, array( 'someColumn = ?' => $whatever ) );
$db->commit();
}
catch( Exception $e )
{
$db->rollBack();
}
Or simply issue 'raw' SELECT ... FOR UPDATE and UPDATE SQL statements on $db of course.

Related

Doctrine and PDO not giving the same results

Okay, the code below I have created to show the issue.
And yes, I appreciate the fact that it should ALL be doctrine and by using PDO it makes the whole doctrine thing kinda pointless.
More importantly though, I am trying to understand why the two don't match up.
Basically, the first PDO query comes back with: 70 row count and 140 Max ID.
The second PDO query returns the same 70 row count and 140 Max ID.
The problem is, the doctrine insert between the two has made it 71 and 141.
Why is the second PDO query note reflecting this?
If I run the query in a separate PHP page or in Workbench, it returns 71/141. It just doesn't within the executing page.
$userID = $enUser->getId();
$stmt1 = $this->pdoConnection->getConnection()->prepare('
SELECT COUNT(UT.ID) AS UTC,
MAX(UT.ID) AS UTMID
FROM UserThing UT
WHERE UT.User_ID = :userID;
' ) or die ( implode( ':', $stmt1->errorInfo( ) ) );
$stmt1->bindParam( ':userID', $userID, \PDO::PARAM_INT, 11 );
$results = $this->pdoConnection->dbRecSet( $stmt1, false );
$stmt1->closeCursor();
unset($stmt1);
var_dump($results);
$newUserThing = new UserThing();
$newUserThing->setUserThingThings( $enThing );
$newUserThing->setUserThingUsers( $enUser );
$newUserThing->setAttainedDate( new \DateTime() );
$this->em->persist( $newUserThing );
$this->em->flush();
$userThingID = $newUserThing->getId();
echo $userThingID;
$stmt2 = $this->pdoConnection->getConnection()->prepare('
SELECT COUNT(UT.ID) AS UTC,
MAX(UT.ID) AS UTMID
FROM UserThing UT
WHERE UT.User_ID = :userID;
' ) or die ( implode( ':', $stmt2->errorInfo( ) ) );
$stmt2->bindParam( ':userID', $userID, \PDO::PARAM_INT, 11 );
$results = $this->pdoConnection->dbRecSet( $stmt2, false );
$stmt2->closeCursor();
unset($stmt2);
var_dump($results);
Any ideas why the second PDO query doesn't reflect the DB change, even though Doctrine is able to return the newly inserted ID?
The bit which didn't occur to me when making this post, was the fact that my PDO class used a different connection string to the Symfony one.
To be able to support multiple record sets to be returned from a single query, my class used different driver options to the default. So I just ported over the class into Symfony and used it and it worked well for a long time.
That said, doing an insert/update on one connection and reading straight after on another does not work well. That is the issue I found here.
The documentation for Symfony when it comes to PDO driver options is a bit lacking. For anyone facing it, all you need to know is that the PDO constants just need their Integer equivalents in the YML instead.
So you end up with things like this in your app/config/config.yml:
options:
20: true
3: 1
1002: "SET NAMES 'utf8' COLLATE 'utf8_unicode_ci'"
That allowed me to do what I needed to and do it correctly.

query and UPDATE in cakephp1.3

would like to update attached_blog_id field to set NULL. Thank you in advance.
foreach($this->Event->find('all', array('conditions' => array('Event.attached_blog_id' => $this->id()))) as $attached_blog)
$this->Event->query('UPDATE armo8427_events' . ' SET attached_blog_id = NULL' . ' WHERE id = ' . $attached_blog['Event']['id']);
Don't use query unnecessarily
The code in the question would be better written as:
$events = $this->Event->find(
'all',
array(
'conditions' => array(
'Event.attached_blog_id' => $this->id()
)
)
);
foreach($events as $event) {
$this->Event->id = $event['Event']['id'];
$this->Event->saveField('attached_blog_id', null);
}
With the code-logic in the question there is no need to use query at all
But be efficient
The logic in the question can be expressed as a single sql query, instead of 1+n:
UPDATE
events
SET
attached_blog_id = NULL
WHERE
attached_blog_id = $id;
I.e. if there were 100 linked blogs, using the foreach loop will issue 101 queries, wheras this is the same in one query irrespective of the number rof affected rows.
The most appropriate way to do that in CakePHP is to use updateAll:
$id = $this->id(); // From the question
$this->Event->updateAll(
array('Baker.attached_blog_id' => null), // the update
array('Baker.attached_blog_id' => $id) // conditions to match
);
the method query should be reserved for sql calls which are not possible to achieve using the provided model methods.

Doctrine DBAL Insert if doesn't exist

I'm using Symfony2-framework with Doctrine DBAL, and I'm inserting some data into MySql-database. Insert looks something like this(simplified):
$conn->insert('sometable', array(
"col1" => $data1,
"col2" => $data2,
"col3" => $data3
));
What I would like to achieve is the functionality like the ordinary sql-has. The ability to insert if doesn't exist, like: INSERT IGNORE. But is it possible to do such a thing with DBAL?
Note that I don't use objects here.
Edit: Please do note that I'm not using objects, but rather the depicted array-insert-method of DBAL.
Edit2: I tried to approach the problem with using the suggested try-catch, which seems to work quite well except for one thing. The db auto increments the primary key even if no new rows were added.
Here is the code that I used:
try{
$conn->insert('sometable', array(
"col1" => $data1,
"col2" => $data2,
"col3" => $data3
));
} catch( \Exception $e) {
switch (get_class($e)) {
case 'Doctrine\DBAL\DBALException':
// No problems here. Just means that the row already existed.
break;
default:
$this->get('logger')->error("(ERROR in ".__METHOD__.", #Row: ".(__LINE__)."): DB-error! error: ".$e->getMessage());
break;
}
}
And I also had to do a multiple row unique index for the table, because I have to check if all the columns are the same. i.e. if the whole row is the same as the one we are trying to insert.
So.. It works well otherwise, except that the auto increment value keeps rising up every time we try-insert-catch. I don't think it's a real problem, but it just feels stupid to waste numbers.. :D
I'm not aware of a way to do this other than just doing a select query and try to retrieve the row, i.e:
$sql = "SELECT * FROM sometable WHERE ....";
$stmt = $conn->prepare($sql);
$stmt->bindParam(); //or bindValue...
$stmt->execute();
$exists = $stmt->fetch();
if(!$exists) {
// insert or whatever..
} else {
// do nothing?
}
Eventually you might be able to attach Event which would do that for you, but I'm not sure if that applies in your case.
Alternatively you could create an unique constraint and do a try/catch when inserting your data. Whenever the data is not unique, the database would return an error and you'd usually receive a PDOException although in Doctrine I think it's with a different name (check exceptions). Just catch that exception and do nothing with it. Example:
try {
$conn->insert('sometable', array(
"col1" => $data1,
"col2" => $data2,
"col3" => $data3
));
} catch(PDOException $e) {
// do nothing.
}

PHP RedBean store bean if not exist one

I am a little confused. I actively use PHP RedBean as ORM within my direct mail service and I run into curious situation - I have a table with unique key constraint (i.e. subscriber_id, delivery_id) and two scripts that is writing data into this table.
There is source code that is inserting or updating table:
public static function addOpenPrecedent($nSubscriberId, $nDeliveryId)
{
$oOpenStatBean = \R::findOrDispense('open_stat', 'delivery_id = :did AND subscriber_id = :sid', array(':did' => $nDeliveryId, ':sid' => $nSubscriberId));
$oOpenStatBean = array_values($oOpenStatBean);
if (1 !== count($oOpenStatBean)) {
throw new ModelOpenStatException(
"Ошибка при обновлении статистики открытий: пара (delivery_id,
subscriber_id) не является уникальной: ($nDeliveryId, $nSubscriberId).");
}
$oOpenStatBean = $oOpenStatBean[0];
if (!empty($oOpenStatBean->last_add_dt)) {
$oOpenStatBean->precedent++;
} else {
$oOpenStatBean->delivery_id = $nDeliveryId;
$oOpenStatBean->subscriber_id = $nSubscriberId;
}
$oOpenStatBean->last_add_dt = time('Y-m-d H:i:s');
\R::store($oOpenStatBean);
}
It is called both from two scripts. And I have issues with corruption unique constraint on this table periodically, because race conditions occurs. I know about SQL "INSERT on duplicate key update" feature. But how can I obtain same result purely using my ORM?
Current, that I know if, Redbean will not issue an
INSERT ON DUPLICATE KEY UPDATE
as the discussion of this cited in the comments above indicates that Redbean's developer considers upsert to be a business logic thing that would pollute the ORM's interphase. This being said, it is most likely achievable if one were to extend Redbean with a custom Query Writer or plugin per the Documentation. I haven't tried this because the method below easily achieves this behavior without messing with the internals and plugins of the ORM, however, it does require that you use transactions and models and a couple of extra queries.
Basically, start your transaction with either R::transaction() or R::begin() before your call to R::store(). Then in your "FUSE"d model, use the "update" FUSE method to run a query that checks for duplication and retrieves the existing id while locking the necessary rows (i.e. SELECT FOR UPDATE). If no id is returned, you are good and just let your regular model validation (or lack thereof) continue as usual and return. If an id is found, simply set $this->bean->id to the returned value and Redbean will UPDATE rather than INSERT. So, with a model like this:
class Model_OpenStat extends RedBean_SimpleModel{
function update(){
$sql = 'SELECT * FROM `open_stat` WHERE `delivery_id`=? AND 'subscriber_id'=? LIMIT 1 FOR UPDATE';
$args = array( $this->bean->deliver_id, $this->bean->subscriber_id );
$dupRow = R::getRow( $sql, $args );
if( is_array( $dupRow ) && isset( $dupRow['id'] ) ){
foreach( $this->bean->getProperties() as $property => $value ){
#set your criteria here for which fields
#should be from the one in the database and which should come from this copy
#this version simply takes all unset values in the current and sets them
#from the one in the database
if( !isset( $value ) && isset( $dupRow[$property] ) )
$this->bean->$property = $dupRow[$property];
}
$this->bean->id = $dupId['id']; #set id to the duplicates id
}
return true;
}
}
You would then modify the R::store() call like so:
\R::begin();
\R::store($oOpenStatBean);
\R::commit();
or
\R::transaction( function() use ( $oOpenStatBean ){ R::store( $oOpenStatBean ); } );
The transaction will cause the "FOR UPDATE" clause to lock the found row or, in the event that no row was found, to lock the places in the index where your new row will go so that you don't have concurrency issues.
Now this will not solve one user's update of the record clobbering another, but that is a whole different topic.

Zend Framework Database Insert Issue

I'm starting out using the Zend Framework and have created a suitable model which saves data back to a database table. The issue I am having is that the sql statement is trying to insert '?' as the value for each column in the database. I have created the following save function which passes an array of data to the DBtable adapter functions:
public function save() {
$data = $this->getData();
if ($data['pageId']==0) {
$this->getDbTable()->insert($data);
} else {
$this->getDbTable()->update($data, array('pageId = ?' => $data['pageId']));
}
}
This seems to go through the appropriate motions but the item is not added to the database and the sql statement within MySql logs looks something like:
insert into DB_Table ('pageId','title','body') values ('?', '?', '?');
Not quite sure where this is falling down, any pointers would be gratefully received.
Thanks
Data should be in next format:
$data = array(
'pageId' => 1,
'title' => 'title',
'body' => 'body'
);
Are you sure that $this->getDbTable() returns your db adapter?
Try to use:
$db = new Zend_Db_Table('table_name');
$db->insert($data);

Categories