您的位置:首页 > 大数据 > 人工智能

Unit of work, Transactions and Grails

2015-09-15 09:25 537 查看
Working with Groovy and Grails often gives you the feeling that things are magic and when you dive in, you realize that things are more complex than expected. At the same time, you often realize that a reasonable default behavior has been chosen by Groovy/Grails framework: What about transactions’ magic in Grails? For me it was hard to believe so let’s try to understand a little more how things work.

For this, Grails documentation is a bit sparse but you had better go and read it to know how and where to declare transactions. We won’t talk about these details here, you can find them in the documentation.

A last remark if you want to play with the different examples, you can find the code in our SVN repository here. The project also includes tests to illustrate the different issues illustrated below.

It’s not that magic

Imagine a classical example, a banking application. You have built your application incrementally and at a few places you have put some business code in your controllers’ actions because the code was too simple to refactor it in a service. You might come with some code similar to this one:

AccountController.groovy

class AccountController {

def transfer = {

def fromAccount = params?.fromAccount
def toAccount = params?.toAccount
def amountToTransfer = params?.amount

// some validity checks, could be performed through a Command Object class
...

// withdraw from the source account
fromAccount.balance -= amountToTransfer
fromAccount.save(flush:true)

// some logic that raises an error or throws an exception
...

// transfer to the target account
toAccount.balance += amountToTransfer
toAccount.save(flush:true)
}
}


In this case, what happens if any error or exception occurs in the ‘some logic’ block? The transfer has been partially performed and committed in the database and a few hundred dollars is lost in the wilderness.

If you know Grails well you could answer that the ‘flush:true’ is, in this particular case, useless and that without it, everything would have worked, and you would be right (see section ‘Hibernate Session in the equation…’ below). But let’s stick to this example for its simplicity, because there are also more complex cases where the flush of the session is automatically performed by Hibernate.

TO REMEMBER

The important thing to remember is that Grails does no automatically wrap each ‘request’ (treatment associated with a HTTP request) in a transaction, so you are not as safe as expected.

At this point you should ask yourself: what is the correct way to handle transactions in Grails?

Grails Model – Business Logic in Services

And you are right. Grails paradigm is to avoid putting business code in the Controller layer but rather in the model or service layer. That’s why all Grails magic for making transactions transparent only happens if you are sticking to this paradigm.

Imagine you refactor the code below in a service (grails-app/services directory):

TransferService.groovy

class TransferService {

def transfer(fromAccount, toAccount, amount) {

// withdraw from the source account
fromAccount.balance -= amountToTransfer
fromAccount.save(flush:true)

// some logic that raises an error or throws an exception
...

// transfer to the target account
toAccount.balance += amountToTransfer
toAccount.save(flush:true)
}
}


and that you use the standard Grails mechanism to inject this service in AccountController:

AccountController.groovy

class AccountController {

def transferService

def transfer = {

...

// call service to make the transfer
transferService.transfer(fromAccount, toAccount, amountToTransfer)
}
}


This time you will get the expected behavior which is, the balance of the first account won’t be modified and committed if an error happens after the first save(flush:true).

Why? Here comes Grails magic. by default every call to a Service’s method is made transactional. How does this work? Using a combination of Spring IoC for injecting an instance of a service in the controller and using the Proxy pattern to make sure the call is enrolled in a transaction. This behavior is controlled by the Service property ‘transactional’ which is set to true by default.

So if you modify you service code to :

class TransferService {
static transactional = false

...
}


you will go back to the phony behavior.

There are a few pitfalls you should avoid when using services, which are:

do no create instances of the service, always use injection. If you do:

class AccountController {

def transferService = new TransferService()
...
}


In this case the calls to TransferService won’t be proxied so you won’t get any transactional behavior.

do not use closures in services, always use methods

If you were declaring transfer() as a closure like:

class TransferService {

def transfer = { fromAccount, toAccount, amount ->
...
}
}


In this case the call won’t be caught by Spring AOP so it won’t be enrolled in a transaction.

Lastly, take great care of what exception you throw because checked exception do not cause the transaction to roll back by default. In Groovy this could be pretty confusing because you are not forced to declare or catch checked exception. To avoid this, you can configure which exceptions cause a roll back at Spring level (using rollbackFor in the @Transactional annotation for example, see this part of Spring documentation).

TO REMEMBER

use services to factor common business logic AND handle correct transaction behavior

always use injection to call a service

do not use closures to define a service method

use runtime exceptions if you want to roll back a transaction

There are other mechanisms to make use of transactions in Grails, mainly through the withTransaction() method but it is not the recommended approach.

You could also have a more fine-grained transaction management if you use Spring @Transaction annotations, but you would leave the magical world in this case.

I am putting all my business code in services, am I safe?

Actually you might. But you better check and understand better the transaction model.

Let me show you an example which surprisingly doesn’t work:

class AccountController {

def transferService
def auditService

def transfer = {

def fromAccount = params?.fromAccount
def toAccount = params?.toAccount
def amountToTransfer = params?.amount

// some validity checks, could be performed through a Command Object class
// ...

// call service to make the transfer
transferService.transfer(fromAccount, toAccount, amountToTransfer)

// notify the auditing plateform of the transfer
auditService.notifyTransfer(fromAccount, toAccount, amountToTransfer)
}
}


So what? Imagine the notifyTransfer() call fails (audit server might be down), an exception is then thrown and the transaction is rolled back. But the question is: which transaction? Because actually there are two different transactions in this transfer() action. One around the transferService() and one around the notifyTransfer() call. Oops! Which takes us to our initial finding: it’s not that magic…

Why are there two transactions? We won’t talk long about this but to make the story short, the default transaction propagation on services’ methods is REQUIRED (see this doc) so a new transaction is created when entering transfer() and committed when exiting transfer(). The same applies to the notifyTransfer() call. See picture below for a better understanding of what is going on:



Doc’, what should I do?

The obvious answer is to stick to Grails paradigm and make sure your unit of work is always encapsulated in the same transaction. In the case above, the natural way of solving it would be to put the notifyTransfer() call in the transferService.transfer() service call. If this breaks your object encapsulation, you will have to create a new service that will take care of calling both transfer() and notifyTransfer().

TO REMEMBER

Stick to Grails paradigm of putting transactional code in the services

Always put your unit of work in a top-level service, aka don’t call two services in a controller action

Hibernate Session in the equation…

When Hibernate session comes into the game, everything mentioned above becomes true or false. As a quick reminder, the session management in Grails is based on the Open Session In View pattern (OSIV, see this doc). The thing to remember is that Hibernate Session is not flushed if an exception is thrown which is the reason why we put save(flush:true) in the first example.

This enables you to put read/write code in your controller action without fearing of having an inconsistent state in the database but it is actually a false sense of security. Indeed, this feature falls apart when your are calling services because committing a transaction flushes your Hibernate session. Or when you really have to flush the session because you need to.

Another pitfall occurs if you read a state in your controller action (thus outside of any transaction) and you then call a transactional service. You then loose the isolation level defined at your transaction level, which is for sure not what you wanted at first.

TO REMEMBER

It is OK to rest on Hibernate session and to put simple logic in your controller, as long as you don’t call any service in your controller.

You should not load objects outside of a transactional service and use it as an argument provided to the service

Quest for a better world…

Isn’t there any easier way, any more magic, that could turn transaction management in a really reckless job, at least in Grails?

One solution could be to use the ‘one request one transaction’ pattern, in the same way Grails use the OSIV pattern. The trick would be to open the transaction before the controller action is called, and to commit it after the action returns (but before the view is rendered).

One way to do this could be to add a filter taking care of starting and committing the transaction. For example, you could add a TransactionFilters in grails-app/conf directory:

TransactionFilters.groovy

class TransactionFilters {

def sessionFactory

def filters = {

startTransaction(controller:'*', action:'*') {
before = {
sessionFactory.getCurrentSession().beginTransaction()
}
}

// no need for an 'after' filter, Spring take care of committing or rolling back
// the transaction

}

}


With this filter in place, the schema shown in Figure 1. will become:



What impact does it have? This would definitively have a very small impact on performance as the ability to make use of read-only transactions or non-transactional requests would be lost. But in my opinion the performance gain is so minor that you would definitively stop using Groovy and Grails if you really had such a performance critical application in production. Of course all this is not applicable to the batch processing part of your application. In that case, the transaction management will be completely different and not tied to any HTTP request, but this is out of the scope of this article.

One last point: even if this filter does simplify transaction management, it’s always better to have a good understanding of how transactions work and not just to rely on too much magic.

That’s it, I hope I did not lose you on the way. And feel free to leave a comment and ask for clarification.

补充一点,grails默认的事务声明是在service层声明,并且默认开启,该service的所有方法都会嵌套事务,但是方法间的事务是隔断的,如果service中有方法间的调用,并且两个方法中都有对数据库的操作,则当一个方法中产生异常而不抛出时,另一个方法是不受影响的,即不会事务回滚,如果要关联事务,必须在被调用的方法中抛异常至调用该方法的方法,然后catch,这样才能实现同事务回滚
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: