How to use transactions in Yii event - yii

I know, how to use transactions in pure DAO or in ActiveModel, where transaction is initiated before call to $model->save() and rolled back upon any exception.
But how to use transactions, if the only place of code I have access to (no matter, why) is Yii event?
public function beforeDelete()
{
foreach($this->menuItems as $menuItem) $menuItem->delete();
return parent::beforeDelete();
}
If I initiate transaction there, capture possible exception and rollback entire transaction upon it, then only deletion of relational models (here: menu items) will be rolled back. It will not prevent (roll back) deletion of master record.
Does preventing deletion of master record, by returning FALSE in my own beforeDelete in case of exception, is all I need to take care here? Or should I avoid transactions at all in Yii events?

What about override save method:
public function save($runValidation=true,$attributes=null)
{
$transaction=$this->getDbConnection()->beginTransaction();
try
{
$result = parent::save($runValidation,$attributes);
if($result)
$transaction->commit();
else
$transaction->rollback();
}
catch(Exception $e)
{
$transaction->rollback();
$result = false;
}
return $result;
}

Answering my own question with example piece of code to further extend my comment given to Alex's answer:
public function beforeDelete()
{
$transaction = $this->getDbConnection()->beginTransaction();
try
{
foreach($this->menuItems as $menuItem) $menuItem->delete();
$transaction->commit();
return parent::beforeDelete();
}
catch(Exception $ex)
{
$transaction->rollback();
return FALSE;
}
}
Both answers seems correct, both are alternative to each other. Though, I accept Alex answer, as better.

Related

Using action filters to perform transactions

I want to make a TransactionFilter in Yii to be applied over an action to wrap it in a transaction so I don´t have to write the same code over and over every time I want to use transactions, at least that's the idea. I have
class TransactionFilter extends CFilter
{
public function filter($filterChain)
{
if(Yii::app()->getRequest()->getIsPostRequest())
{
$transaction= Yii::app()->db->beginTransaction();
try {
$filterChain->run();
$transaction->commit();
}catch(Exception $e) {
$transaction->rollback();
}
}
else
$filterChain->run();
}
}
This is my filters method in my User class:
public function filters()
{
return array(
'accessControl',
'postOnly + delete',
array('application.components.TransactionFilter + create'),
);
}
I'm assuming $filterChain->run() will eventually execute the action but the problem arises when there's a redirect in the action, it never made it after the $filterChain->run() sentence in the filter
I don't know if this approach would be advisable and posible in Yii, if not I would appreciate the help if there is another approach or I have to stick with the traditional one.
Thank you.
You have to begin transaction on:
protected function preFilter($filterChain)
And commit, rollback on :
protected function postFilter($filterChain)

Yii Call beforeSave() in other model's beforeSave()

I am trying to save a record of another model in model's beforeSave() method. I expected the save() method to call the other's model beforeSave(),but it doesn't.Is it a bug or I am just missed something?Here is some code.
Operations model
public function beforeSave(){
if(parent::beforeSave()){
if($this->isNewRecord){
$this->creation_date=$this->modification_date=time();
$cur=Currencies::model()->find('LOWER(short)=?',array('usd'));
if($cur->id!=$this->currency_id){
$conv_cours=Currencies::model()->findByPk($this->currency_id);
$this->ammount_usd=round(($this->ammount*$conv_cours->buy)/$cur->sell,4);
}else{
$this->ammount_usd=$this->ammount;
}
}else{
$this->modification_date=time();
}
$opType=OperationType::model()->findByPk($this->operation_type);
$log=new ActionLogs;
$log->comment=Yii::app()->user->name.' добавил '.$opType->name;
/*$log->logtime=time();
$log->user_id=Yii::app()->user->id;*/
$v=1;
$log->save ();
return true;
}else{
return false;
}
}
And another models beforeSave()
public function beforeSave(){
if(parent::beforeSave()){
if($this->isNewRecord){
$this->logtime=time();
$this->user_id=Yii::app()->user->id;
}
return true;
}else{
return false;
}
}
Thanks.
The only reason I can think of for Yii not calling beforeSave when you try to save an activerecord is when its validation fails. Either try to do a dump of $object->errors after your save call or try to save it with $object->save(FALSE) to skip validation. Like that we can eliminate this as a cause.

Rollback transaction in Repository from another class

well my problem is:
I have a method like:
class Manager
{
void method1()
{
// save object in database to get ID
int newId = this.Repository.Save(obj);
try {
// call remote webservice to save another object with same ID as in local DB
webservice.Save(remoteObj, id);
}
catch(Exception e)
{
// do Rollback in Repository here
}
}
}
Bassically this is the code. Repository use NHibernate to save to DB. I need to save in DB to know the new ID and then send this ID to webservice. If something fail calling webservice I want to rollback and discard saved object.... and here is my problem. I can't open and control a transaction in Repository from my class Manager.
I already try with this also:
class Manager
{
void method1()
{
using (TransactionScope scope = new TransactionScope())
{
// save object in database to get ID
int newId = this.Repository.Save(obj);
// call remote webservice to save another object with same ID
// as in local DB
webservice.Save(remoteObj, id);
scope.Complete();
}
}
}
Here the problem is that the rollback is OK but not the Save(Create in NHibernate). I get error about that object "Transaction" is not found or the transaction is already closed just after the line : "scope.Complete();".
I think that something is wrong trying to control NHibernate transaction with TransactionScope .
I dont know if is a problem about approach, maybe another way should be used to handle this situation... ??
any help or idea where to find ??
Thanks a lot !!
Assuming you already have an opened session in a CurrentSession property/variable and that you could pass that working session to your repository, I would do the following:
using(var trx = CurrentSession.BeginTransaction())
{
try
{
int newId = this.Repository.Save(obj, CurrentSession);
webservice.Save(remoteObj, id);
trx.Commit();
}
catch
{
trx.Rollback();
}
}

Asynchronous queries in a web app, using NHibernate

In a web application, the Session is only available in the current thread.
Does anyone have any tips for executing queries through NHibernate in a new asynchronous thread?
For example, how could I make something like this work:
public void Page_Load()
{
ThreadPool.QueueUserWorkItem(state => FooBarRepository.Save(new FooBar()));
}
You need to have a session context that's smart enough for non web context. But more importantly, the new thread should have it's own session.
You can use something like the following:
private ISession threadSession
{
get
{
if (HttpContext.Current != null)
{
return (ISession)HttpContext.Current.Items["THREAD_SESSION"];
}
return (ISession)AppDomain.CurrentDomain
.GetData("THREAD_SESSION" + Thread.CurrentThread.ManagedThreadId);
}
set
{
if (HttpContext.Current != null)
{
HttpContext.Current.Items["THREAD_SESSION"] = value;
}
else
{
AppDomain.CurrentDomain.SetData("THREAD_SESSION"
+Thread.CurrentThread.ManagedThreadId, value);
}
}
}
Sessions are not thread-safe. IOW you'll run into issues sooner or later if you create a session on one thread and use it from another. Create a new session on your background thread and close it before your background thread finishes.
How about:
public void Page_Load()
{
ThreadPool.QueueUserWorkItem(state => NHibernateSessionFactory.GetSession().Save(new FooBar()));
}

Automatic Schema Validation using NHibernate/ActiveRecord

Let's assume I have a table of Products with columns: Id, Name, Price
and using NHibernate (or ActiveRecord) I map the table to the POCO:
public class Product
{
public virtual long Id { get; set; }
public virtual string Name { get; set; }
public virtual double Price { get; set; }
}
Now if someday a new column named ShipmentPrice (let's assume it's double too)
will be added to the Products table, is there any way I can automatically know that?
For saying automatically I mean adding code to do that or getting an exception?
(I assume I don't have control on the columns of the table or a way to
know of any changes to the table's schema in advance)
You do recall correctly, Mauricio. The following code shows how you can create or update a schema. The update will run when Validate() raises an exception. No exception will be thrown when a field is available in the database but not in the configuration. It is perfectly legal to have extra fields: you don't want them to be deleted, I hope? That could cause tremendous damage...
The following code shows Test, Create, Validate and Update, each step with the proper exception handling. The code is simplified, but it should give you a handle on how to do a validation.
This code helps with Entity-centric (POCO) ORM configurations, where you can add a field to your class and it will automatically be updated in the database. Not with table-centric, where fields are leading.
// executes schema script against database
private static void CreateOrUpdateSchema(Configuration config)
{
// replace this with your test for existence of schema
// (i.e., with SQLite, you can just test for the DB file)
if (!File.Exists(DB_FILE_NAME))
{
try
{
SchemaExport export = new SchemaExport(config);
export.Create(false, true);
}
catch (HibernateException e)
{
// create was not successful
// you problably want to break out your application here
MessageBox.Show(
String.Format("Problem while creating database: {0}", e),
"Problem");
}
}
else
{
// already something: validate
SchemaValidator validator = new SchemaValidator(config);
try
{
validator.Validate();
}
catch (HibernateException)
{
// not valid, try to update
try
{
SchemaUpdate update = new SchemaUpdate(config);
update.Execute(false, true);
}
catch (HibernateException e)
{
// update was not successful
// you problably want to break out your application here
MessageBox.Show(
String.Format("Problem while updating database: {0}", e),
"Problem");
}
}
}
}
-- Abel --
You could use NHibernate's SchemaValidator, but IIRC it only checks that your mapped entities are valid so it doesn't check if there are more columns than mapped properties since that wouldn't really break your app.