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
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
and that you use the standard Grails mechanism to inject this service in AccountController:
AccountController.groovy
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 :
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:
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:
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:
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
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,这样才能实现同事务回滚
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,这样才能实现同事务回滚
相关文章推荐
- max-min fairness 最大最小公平算法
- LeetCode-Contains Duplicate
- 系统调用跟我学之wait, waitpid函数
- 启动2015世界人工智能系统智商排名,检测人工智能是否超越人类
- 算法系列--Climbing Stairs
- http://blog.csdn.net/u011975949/article/details/46868373
- 报表XML导出rtf格式,结果在浏览器中打开XML文件。下载rtf文件打开后出现Authentication failed 问题
- rails
- stackstack pillar and grains
- leetcode 220 Contains Duplicate III
- linux 中/proc 详解 http://blog.csdn.net/kevinx_xu/article/details/8178746
- main函数可否被递归调用
- poj1363Rails(栈模拟)
- jedis高版本,JedisPoolConfig,maxActive属性,maxWait,配置maxActive,maxTotal,maxWaitMillis
- AIX常用
- POJ1681 Painter's Problem【高斯消元法】
- Climbing Stairs
- 配置sendmail转发邮件
- jerbrains产品算号
- OnInitUpdate、OnUpdate、OnDraw与OnPaint!