Skip to content
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
105 changes: 105 additions & 0 deletions core/DB/DB.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ abstract class DB
* flag to indicate whether we're in transaction
**/
private $transaction = false;
/**
* @var list of all started savepoints
*/
private $savepointList = array();

private $queue = array();
private $toQueue = false;
Expand Down Expand Up @@ -140,6 +144,7 @@ public function commit()
$this->queryRaw("commit;\n");

$this->transaction = false;
$this->savepointList = array();

return $this;
}
Expand All @@ -155,6 +160,7 @@ public function rollback()
$this->queryRaw("rollback;\n");

$this->transaction = false;
$this->savepointList = array();

return $this;
}
Expand Down Expand Up @@ -222,6 +228,69 @@ public function isQueueActive()
}
//@}

/**
* @param string $savepointName
* @return DB
*/
public function savepointBegin($savepointName)
{
$this->assertSavePointName($savepointName);
if (!$this->inTransaction())
throw new DatabaseException('To use savepoint begin transaction first');

$query = 'savepoint '.$savepointName;
if ($this->toQueue)
$this->queue[] = $query;
else
$this->queryRaw("{$query};\n");

return $this->addSavepoint($savepointName);
}

/**
* @param string $savepointName
* @return DB
*/
public function savepointRelease($savepointName)
{
$this->assertSavePointName($savepointName);
if (!$this->inTransaction())
throw new DatabaseException('To release savepoint need first begin transaction');

if (!$this->checkSavepointExist($savepointName))
throw new DatabaseException("savepoint with name '{$savepointName}' nor registered");

$query = 'release savepoint '.$savepointName;
if ($this->toQueue)
$this->queue[] = $query;
else
$this->queryRaw("{$query};\n");

return $this->dropSavepoint($savepointName);
}

/**
* @param string $savepointName
* @return DB
*/
public function savepointRollback($savepointName)
{
$this->assertSavePointName($savepointName);
if (!$this->inTransaction())
throw new DatabaseException('To rollback savepoint need first begin transaction');

if (!$this->checkSavepointExist($savepointName))
throw new DatabaseException("savepoint with name '{$savepointName}' nor registered");

$query = 'rollback to savepoint '.$savepointName;
if ($this->toQueue)
$this->queue[] = $query;
else
$this->queryRaw("{$query};\n");

return $this->dropSavepoint($savepointName);
}

/**
* base queries
**/
Expand Down Expand Up @@ -331,5 +400,41 @@ public function setEncoding($encoding)

return $this;
}

/**
* @param string $savepointName
* @return DB
*/
private function addSavepoint($savepointName)
{
if ($this->checkSavepointExist($savepointName))
throw new DatabaseException("savepoint with name '{$savepointName}' already marked");

$this->savepointList[$savepointName] = true;
return $this;
}

/**
* @param string $savepointName
* @return DB
*/
private function dropSavepoint($savepointName)
{
if (!$this->checkSavepointExist($savepointName))
throw new DatabaseException("savepoint with name '{$savepointName}' nor registered");

unset($this->savepointList[$savepointName]);
return $this;
}

private function checkSavepointExist($savepointName)
{
return isset($this->savepointList[$savepointName]);
}

private function assertSavePointName($savepointName)
{
Assert::isEqual(1, preg_match('~^[A-Za-z][A-Za-z0-9]*$~iu', $savepointName));
}
}
?>
113 changes: 113 additions & 0 deletions core/DB/Transaction/InnerTransaction.class.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
<?php
/***************************************************************************
* Copyright (C) 2012 by Alexey S. Denisov *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License as *
* published by the Free Software Foundation; either version 3 of the *
* License, or (at your option) any later version. *
* *
***************************************************************************/

/**
* Utility to create transaction and not think about current nested level
*
* @ingroup Transaction
**/
final class InnerTransaction
{
/**
* @var DB
**/
private $db = null;
private $savepointName = null;
private $finished = false;

/**
* @param DB|GenericDAO $database
Copy link
Member

Choose a reason for hiding this comment

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

на уровне идеи - а может сделать интерфейс, который бы реализовывали оба этих класса и ожидать именно его. DBFull, например.

Copy link
Member

Choose a reason for hiding this comment

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

Почему бы и нет, к примеру DBTransactional, также в диалекте реализовать эти методы и вроде как бы гуд должно получится :-)

Copy link
Member Author

Choose a reason for hiding this comment

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

Это сделано было для упрощения работы с классом. Попробую пояснить ход мыслей, когда именно так сделал
Реально классу нужно иметь именно DB под рукой, т.к. именного у него вызывает begin, commit, rollback. Но обычно мы работаем с объектами и у них под боком есть DAO. Из DAO можно вытащить DB, вот так: $db = DBPool::GetByDao(TestObject::dao()); На N'ый раз уже жутко раздражает этот копипаст.
Можно было бы всегда принимать только DAO и из него доставать DB, но это не хорошо, т.к. нет нет, да захочется вдруг передать именно DB и тогда жуть как сложно придется выкручиваться. Сделать два метода с чуть разными названиями и разными аргументами тоже не хочется, т.к. это вносит лишний копипаст и путаницу.
Будь в php перегрузка методов с разными аргументами - не было бы проблем. Собственно по этом и делаем в этом методе вид, что какая-то перегрузка методов есть.
Обий интерфейс придумывать и вешать на классы тоже не хочется ради какой-то новой фичи. Так классы глядишь и обрастут дясятком других интерфейсов. Поэтому я сделал так как оно есть сейчас и усложнять как-то совсем не хочется.

Copy link
Member

Choose a reason for hiding this comment

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

Не знаю что имел в виду Женя, но я понял это как некий интерейс для дтранзакционности в БД, сейчас у нас есть "BaseTransaction" и "DBTransaction" которые нигде не используются, ну собсно понятно почему )))
Вот за место них сделать интерфейс DBTransactional и дальше этот интефейс имплементировать в сам дравер и диалект )))

Лично я так подумал.

Copy link
Member

Choose a reason for hiding this comment

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

Я имел ввиду то, что описал @AlexeyDsov ;)
Ну нет, так нет, это не так и принципиально.

Copy link
Member

Choose a reason for hiding this comment

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

Ну значит я не в тему описал.
Ну я за то что сейчас есть ))) вроде как оптимально пока ;)

Copy link
Member Author

Choose a reason for hiding this comment

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

Помоему имелось ввиду что-то другое. Про BaseTransaction и DBTransaction я думаю все так же - из-за них begin, commit и т.д. в базе объявлены deprecated но при этом их самих использовать неудобно + inTransaction свойство по ощущениям очень привязано к базе, а за базу отвечает именно DB класс. С другой стороны из DB сложную логику нужно выносить, т.к. вроде бы он достаточно перегружен и всякие наворотам c queue и queueFlush там не особо место, пожалуй.
P.S. пока писал ответы уже появились

Copy link
Member

Choose a reason for hiding this comment

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

Ну собсно о чем и речь, и лучше всего думаю это сделать интерфейсами.

* @param IsolationLevel $level
* @param AccessMode $mode
* @return InnerTransaction
**/
public static function begin(
$database,
IsolationLevel $level = null,
AccessMode $mode = null
)
{
return new self($database, $level, $mode);
}

/**
* @param DB|GenericDAO $database
* @param IsolationLevel $level
* @param AccessMode $mode
**/
public function __construct(
Copy link
Member

Choose a reason for hiding this comment

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

Может быть имеет смысл сделать конструктор приватным? Это бы нам дало одну точку доступа.

Copy link
Member Author

Choose a reason for hiding this comment

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

У нас много где есть create который должно совпадать по аргументам с construct. Тут просто отступил от правила и назвал метод begin, для наглядности, т.к. хорошо ложиться. Но так или иначе при существующем create мы конструктор приватным не делаем.

$database,
IsolationLevel $level = null,
AccessMode $mode = null
)
{
if ($database instanceof DB) {
$this->db = $database;
} elseif ($database instanceof GenericDAO) {
$this->db = DBPool::getByDao($database);
} else {
throw new WrongStateException(
'$database must be instance of DB or GenericDAO'
);
}

$this->beginTransaction($level, $mode);
Copy link
Contributor

Choose a reason for hiding this comment

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

А давай вынесем beginTransaction() в метод begin(), что бы при создании объекта он ничего не писал в базу, а только создавался

Copy link
Member

Choose a reason for hiding this comment

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

А мне текущая реализация нравится :)
$trans = InnerTransaction::begin(); //вполне себе самодокументированно и хорошо.

Copy link
Member Author

Choose a reason for hiding this comment

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

Мне честно говоря самому текущий вариант понравился. Он короче и нагляден и вроде бы нет причин создавать объект раньше чем надо начать трнзакцию.

Copy link
Member

Choose a reason for hiding this comment

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

Логично )))

}

public function commit()
{
$this->assertFinished();
$this->finished = true;
if (!$this->savepointName) {
$this->db->commit();
} else {
$this->db->savepointRelease($this->savepointName);
}
}

public function rollback()
{
$this->assertFinished();
$this->finished = true;
if (!$this->savepointName) {
$this->db->rollback();
} else {
$this->db->savepointRollback($this->savepointName);
}
}

private function beginTransaction(
IsolationLevel $level = null,
AccessMode $mode = null
)
{
$this->assertFinished();
if (!$this->db->inTransaction()) {
$this->db->begin($level, $mode);
} else {
$this->savepointName = $this->createSavepointName();
$this->db->savepointBegin($this->savepointName);
}
}

private function assertFinished()
{
if ($this->finished)
throw new WrongStateException('This Transaction already finished');
}

private static function createSavepointName()
{
static $i = 1;
return 'innerSavepoint'.($i++);
}
}
?>
134 changes: 134 additions & 0 deletions core/DB/Transaction/InnerTransactionWrapper.class.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
<?php
/***************************************************************************
* Copyright (C) 2012 by Alexey S. Denisov *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License as *
* published by the Free Software Foundation; either version 3 of the *
* License, or (at your option) any later version. *
* *
***************************************************************************/

/**
* Utility to wrap function into transaction
*
* @ingroup Transaction
**/
final class InnerTransactionWrapper
{
/**
* @var DB
*/
private $db = null;
/**
* @var StorableDAO
*/
private $dao = null;
private $function = null;
private $exceptionFunction = null;
/**
* @var IsolationLevel
*/
private $level = null;
/**
* @var AccessMode
*/
private $mode = null;

/**
* @return InnerTransactionWrapper
*/
public static function create()
{
return new self;
}

/**
* @param DB $db
* @return InnerTransactionWrapper
*/
public function setDB(DB $db)
{
$this->db = $db;
return $this;
}

/**
* @param StorableDAO $dao
* @return InnerTransactionWrapper
*/
public function setDao(StorableDAO $dao)
{
$this->dao = $dao;
return $this;
}

/**
* @param collable $function
* @return InnerTransactionWrapper
*/
public function setFunction($function)
{
Assert::isTrue(is_callable($function, false), '$function must be callable');
$this->function = $function;
return $this;
}

/**
* @param collable $function
* @return InnerTransactionWrapper
*/
public function setExceptionFunction($function)
{
Assert::isTrue(is_callable($function, false), '$function must be callable');
$this->exceptionFunction = $function;
return $this;
}

/**
* @param IsolationLevel $level
* @return InnerTransactionWrapper
*/
public function setLevel(IsolationLevel $level)
{
$this->level = $level;
return $this;
}

/**
* @param AccessMode $mode
* @return InnerTransactionWrapper
*/
public function setMode(AccessMode $mode)
{
$this->mode = $mode;
return $this;
}

public function run()
{
Assert::isTrue(!is_null($this->dao) || !is_null($this->db), 'set first dao or db');
Assert::isNotNull($this->function, 'set first function');

$transaction = InnerTransaction::begin(
$this->dao ?: $this->db,
$this->level,
$this->mode
);

try {
$result = call_user_func_array($this->function, func_get_args());
$transaction->commit();
return $result;
} catch (Exception $e) {
$transaction->rollback();
if ($this->exceptionFunction) {
$args = func_get_args();
array_unshift($args, $e);
return call_user_func_array($this->exceptionFunction, $args);
}
throw $e;
}
}
}
?>
2 changes: 1 addition & 1 deletion test/config.inc.php.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
'host' => '127.0.0.1',
'base' => 'onphp'
),
'SQLite' => array(
'SQLitePDO' => array(
'user' => 'onphp',
'pass' => 'onphp',
'host' => '127.0.0.1',
Expand Down
Loading