Skip to content

Commit

Permalink
Adds explicit transaction to find and findFirst (phalcon#13226)
Browse files Browse the repository at this point in the history
 - adds setUpTransactionManager in ModelTrait
 - adds missing cache directory to README
 - adds tests for find / findFirst
 - adds const to Model to avoid typos
 - add code examples
 - fixes check before checking multiple different database engine connections
 - unifies businesslogic for find and findFirst
 - adds missing types
 - Adds Querytest And Changelog update
 - removes annotations + phpcs cleanup
  • Loading branch information
chilimatic committed Dec 19, 2017
1 parent 6812d8c commit e7c3287
Show file tree
Hide file tree
Showing 7 changed files with 426 additions and 118 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG-3.3.md
Expand Up @@ -21,3 +21,5 @@
- Fixed `Phalcon\Mvc\Model::allowEmptyStringValues` to correct works with saving empty string values when DEFAULT not set in SQL
- Fixed `Phalcon\Mvc\Model\Behavior\SoftDelete` to correctly update snapshots after deleting item
- Fixed `Phalcon\Mvc\Model` to set old snapshot when no fields are changed when dynamic update is enabled
- Changed `Phalcon\Mvc\Model` to allow to pass a transaction within the query context [#13226](https://github.com/phalcon/cphalcon/issues/13226)
- Added `Phalcon\Mvc\Query::setTransaction` to enable an override transaction [#13226](https://github.com/phalcon/cphalcon/issues/13226)
179 changes: 116 additions & 63 deletions phalcon/mvc/model.zep
Expand Up @@ -14,6 +14,7 @@
+------------------------------------------------------------------------+
| Authors: Andres Gutierrez <andres@phalconphp.com> |
| Eduar Carvajal <eduar@phalconphp.com> |
| Jakob Oberhummer <cphalcon@chilimatic.com> |
+------------------------------------------------------------------------+
*/

Expand Down Expand Up @@ -84,7 +85,6 @@ use Phalcon\Events\ManagerInterface as EventsManagerInterface;
*/
abstract class Model implements EntityInterface, ModelInterface, ResultInterface, InjectionAwareInterface, \Serializable, \JsonSerializable
{

protected _dependencyInjector;

protected _modelsManager;
Expand All @@ -97,7 +97,7 @@ abstract class Model implements EntityInterface, ModelInterface, ResultInterface

protected _dirtyState = 1;

protected _transaction;
protected _transaction { get };

protected _uniqueKey;

Expand All @@ -113,6 +113,8 @@ abstract class Model implements EntityInterface, ModelInterface, ResultInterface

protected _oldSnapshot = [];

const TRANSACTION_INDEX = "transaction";

const OP_NONE = 0;

const OP_CREATE = 1;
Expand Down Expand Up @@ -467,7 +469,7 @@ abstract class Model implements EntityInterface, ModelInterface, ResultInterface
{
var key, keyMapped, value, attribute, attributeField, metaData, columnMap, dataMapped, disableAssignSetters;

let disableAssignSetters = globals_get("orm.disable_assign_setters");
let disableAssignSetters = globals_get("orm.disable_assign_setters");

// apply column map for data, if exist
if typeof dataColumnMap == "array" {
Expand Down Expand Up @@ -809,14 +811,66 @@ abstract class Model implements EntityInterface, ModelInterface, ResultInterface
* foreach ($robots as $robot) {
* echo $robot->name, "\n";
* }
*
* // encapsulate find it into an running transaction esp. useful for application unit-tests
* // or complex business logic where we wanna control which transactions are used.
*
* $myTransaction = new Transaction(\Phalcon\Di::getDefault());
* $myTransaction->begin();
* $newRobot = new Robot();
* $newRobot->setTransaction($myTransaction);
* $newRobot->save(['name' => 'test', 'type' => 'mechanical', 'year' => 1944]);
*
* $resultInsideTransaction = Robot::find(['name' => 'test', Model::TRANSACTION_INDEX => $myTransaction]);
* $resultOutsideTransaction = Robot::find(['name' => 'test']);
*
* foreach ($setInsideTransaction as $robot) {
* echo $robot->name, "\n";
* }
*
* foreach ($setOutsideTransaction as $robot) {
* echo $robot->name, "\n";
* }
*
* // reverts all not commited changes
* $myTransaction->rollback();
*
* // creating two different transactions
* $myTransaction1 = new Transaction(\Phalcon\Di::getDefault());
* $myTransaction1->begin();
* $myTransaction2 = new Transaction(\Phalcon\Di::getDefault());
* $myTransaction2->begin();
*
* // add a new robots
* $firstNewRobot = new Robot();
* $firstNewRobot->setTransaction($myTransaction1);
* $firstNewRobot->save(['name' => 'first-transaction-robot', 'type' => 'mechanical', 'year' => 1944]);
*
* $secondNewRobot = new Robot();
* $secondNewRobot->setTransaction($myTransaction2);
* $secondNewRobot->save(['name' => 'second-transaction-robot', 'type' => 'fictional', 'year' => 1984]);
*
* // this transaction will find the robot.
* $resultInFirstTransaction = Robot::find(['name' => 'first-transaction-robot', Model::TRANSACTION_INDEX => $myTransaction1]);
* // this transaction won't find the robot.
* $resultInSecondTransaction = Robot::find(['name' => 'first-transaction-robot', Model::TRANSACTION_INDEX => $myTransaction2]);
* // this transaction won't find the robot.
* $resultOutsideAnyExplicitTransaction = Robot::find(['name' => 'first-transaction-robot']);
*
* // this transaction won't find the robot.
* $resultInFirstTransaction = Robot::find(['name' => 'second-transaction-robot', Model::TRANSACTION_INDEX => $myTransaction2]);
* // this transaction will find the robot.
* $resultInSecondTransaction = Robot::find(['name' => 'second-transaction-robot', Model::TRANSACTION_INDEX => $myTransaction1]);
* // this transaction won't find the robot.
* $resultOutsideAnyExplicitTransaction = Robot::find(['name' => 'second-transaction-robot']);
*
* $transaction1->rollback();
* $transaction2->rollback();
* </code>
*/
public static function find(var parameters = null) -> <ResultsetInterface>
{
var params, builder, query, bindParams, bindTypes, cache, resultset, hydration, dependencyInjector, manager;

let dependencyInjector = Di::getDefault();
let manager = <ManagerInterface> dependencyInjector->getShared("modelsManager");
var params, query, resultset, hydration;

if typeof parameters != "array" {
let params = [];
Expand All @@ -827,36 +881,7 @@ abstract class Model implements EntityInterface, ModelInterface, ResultInterface
let params = parameters;
}

/**
* Builds a query with the passed parameters
*/
let builder = manager->createBuilder(params);
builder->from(get_called_class());

let query = builder->getQuery();

/**
* Check for bind parameters
*/
if fetch bindParams, params["bind"] {

if typeof bindParams == "array" {
query->setBindParams(bindParams, true);
}

if fetch bindTypes, params["bindTypes"] {
if typeof bindTypes == "array" {
query->setBindTypes(bindTypes, true);
}
}
}

/**
* Pass the cache options to the query
*/
if fetch cache, params["cache"] {
query->cache(cache);
}
let query = static::getPreparedQuery(params);

/**
* Execute the query passing the bind-params and casting-types
Expand Down Expand Up @@ -886,7 +911,7 @@ abstract class Model implements EntityInterface, ModelInterface, ResultInterface
*
* // What's the first mechanical robot in robots table?
* $robot = Robots::findFirst(
* "type = 'mechanical'"
* "type = 'mechanical'"
* );
*
* echo "The first mechanical robot name is ", $robot->name;
Expand All @@ -900,18 +925,27 @@ abstract class Model implements EntityInterface, ModelInterface, ResultInterface
* );
*
* echo "The first virtual robot name is ", $robot->name;
* </code>
*
* @param string|array parameters
* @return static
* // behaviour with transaction
* $myTransaction = new Transaction(\Phalcon\Di::getDefault());
* $myTransaction->begin();
* $newRobot = new Robot();
* $newRobot->setTransaction($myTransaction);
* $newRobot->save(['name' => 'test', 'type' => 'mechanical', 'year' => 1944]);
*
* $findsARobot = Robot::findFirst(['name' => 'test', Model::TRANSACTION_INDEX => $myTransaction]);
* $doesNotFindARobot = Robot::findFirst(['name' => 'test']);
*
* var_dump($findARobot);
* var_dump($doesNotFindARobot);
*
* $transaction->commit();
* $doesFindTheRobotNow = Robot::findFirst(['name' => 'test']);
* </code>
*/
public static function findFirst(var parameters = null) -> <Model>
{
var params, builder, query, bindParams, bindTypes, cache,
dependencyInjector, manager;

let dependencyInjector = Di::getDefault();
let manager = <ManagerInterface> dependencyInjector->getShared("modelsManager");
var params, query;

if typeof parameters != "array" {
let params = [];
Expand All @@ -922,24 +956,45 @@ abstract class Model implements EntityInterface, ModelInterface, ResultInterface
let params = parameters;
}

let query = static::getPreparedQuery(params, 1);

/**
* Return only the first row
*/
query->setUniqueRow(true);

/**
* Execute the query passing the bind-params and casting-types
*/
return query->execute();
}


/**
* shared prepare query logic for find and findFirst method
*/
private static function getPreparedQuery(var params, var limit = null) -> <Query> {
var builder, bindParams, bindTypes, transaction, cache, manager, query, dependencyInjector;

let dependencyInjector = Di::getDefault();
let manager = <ManagerInterface> dependencyInjector->getShared("modelsManager");

/**
* Builds a query with the passed parameters
*/
let builder = manager->createBuilder(params);
builder->from(get_called_class());

/**
* We only want the first record
*/
builder->limit(1);
if limit != null {
builder->limit(limit);
}

let query = builder->getQuery();

/**
* Check for bind parameters
*/
if fetch bindParams, params["bind"] {

if typeof bindParams == "array" {
query->setBindParams(bindParams, true);
}
Expand All @@ -951,24 +1006,21 @@ abstract class Model implements EntityInterface, ModelInterface, ResultInterface
}
}

if fetch transaction, params[self::TRANSACTION_INDEX] {
if transaction instanceof TransactionInterface {
query->setTransaction(transaction);
}
}

/**
* Pass the cache options to the query
*/
if fetch cache, params["cache"] {
query->cache(cache);
}

/**
* Return only the first row
*/
query->setUniqueRow(true);

/**
* Execute the query passing the bind-params and casting-types
*/
return query->execute();
return query;
}

/**
* Create a criteria for a specific model
*/
Expand Down Expand Up @@ -2065,7 +2117,8 @@ abstract class Model implements EntityInterface, ModelInterface, ResultInterface
} else {
let automaticAttributes = metaData->getAutomaticCreateAttributes(this);
}
let defaultValues = metaData->getDefaultValues(this);

let defaultValues = metaData->getDefaultValues(this);

/**
* Get string attributes that allow empty strings as defaults
Expand Down Expand Up @@ -2598,7 +2651,7 @@ abstract class Model implements EntityInterface, ModelInterface, ResultInterface
let bindTypes[] = bindType;
}
}
let newSnapshot[attributeField] = value;
let newSnapshot[attributeField] = value;

} else {
let newSnapshot[attributeField] = null;
Expand Down

0 comments on commit e7c3287

Please sign in to comment.