| Title: |
| Notice: Licensed to the Apache Software Foundation (ASF) under one |
| or more contributor license agreements. See the NOTICE file |
| distributed with this work for additional information |
| regarding copyright ownership. The ASF licenses this file |
| to you under the Apache License, Version 2.0 (the |
| "License"); you may not use this file except in compliance |
| with the License. You may obtain a copy of the License at |
| . |
| http://www.apache.org/licenses/LICENSE-2.0 |
| . |
| Unless required by applicable law or agreed to in writing, |
| software distributed under the License is distributed on an |
| "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| KIND, either express or implied. See the License for the |
| specific language governing permissions and limitations |
| under the License. |
| |
| #Migrating from declarative transactions |
| |
| Many popular application containers, such as Spring or Java EE, offer declarative transaction management. |
| Specifically, the container offers a model where transaction boundaries are defined using metadata, usually as |
| annotations on public methods. |
| |
| ## The Basics |
| |
| A typical transactional method in a declarative model might look like this: |
| |
| @Transactional |
| public Long getCustomerId(String email) { |
| // Some business logic in here... |
| } |
| |
| Using Transaction control the same thing would look like: |
| |
| public Long getCustomerId(String email) { |
| txControl.required(() -> { |
| // Some business logic in here... |
| }); |
| } |
| |
| The main change is that the transactions have moved from being metadata defined, and started by the container, |
| to being code defined and started by the Transaction Control service. This gives several significant advantages. |
| |
| ## The Scoping Problem |
| |
| When using method-level metadata to define transaction boundaries it is not possible to manage transactions |
| on a finer level. This makes it difficult to suspend or nest transactions unless you create a new method to hold |
| the extra logic. |
| |
| ### A Simple Example |
| |
| If we want to write an audit message it usually needs to occur in a new transaction, as typically it should persist |
| even if the overall action fails. In a declarative model this requires a separate method with a new boundary, |
| even if the audit function is a private implementation detail of the Object. |
| |
| @Transactional |
| public void changePassword(String email, String password) { |
| |
| auditPasswordChangeAttempt(email); |
| |
| // Some business logic in here... |
| } |
| |
| @Transactional(REQUIRES_NEW) |
| public void auditPasswordChangeAttempt(String email) { |
| // One line to write the audit message |
| } |
| |
| With transaction control there is no need to create a method just for the internal audit function. |
| |
| @Transactional |
| public void changePassword(String email, String password) { |
| |
| txControl.required(() -> { |
| |
| txControl.requiresNew(() -> { |
| // One line to write the audit message |
| }); |
| |
| // Some business logic in here... |
| }); |
| } |
| |
| |
| ## The Proxy Problem |
| |
| The Proxy problem is a rather insidious issue that affects declarative transactions, and it is a result of how they |
| are implemented. If bytecode weaving is used to directly add transactional behaviour to an object then it will |
| always work the same way. Most solutions, however, do not use bytecode weaving, but instead use a proxy |
| pattern. When a call is made on the proxy the proxy will perform any necessary transaction management |
| before and after delegating the call to the real object. This works well, except if the object makes any internal |
| method calls. |
| |
| In the general case proxying is unsafe because you cannot rely on a method's metadata to decide what |
| transaction state will exist when it is called! |
| |
| ### Examples of the Proxy Problem |
| |
| The following examples are all based on real code migrated from Spring to Transaction Control. |
| |
| #### A Simple Example |
| |
| AbstractPersistenceDataManagerImpl { |
| |
| @Transactional(propagation = SUPPORTS, readOnly = true) |
| public <T> T search(Class<T> entityClass, Object pk, Object params) { |
| return (T) find(entityClass, pk, params); |
| } |
| |
| @Transactional(readOnly = true) |
| public UniWorksSuperDBEntity find(Class entityClass, Object pk, Object params) { |
| return (UniWorksSuperDBEntity) em.find(entityClass, pk); |
| } |
| } |
| |
| In this case the <code>search</code> method does not require a transaction, but delegates to the |
| <code>find</code> method which does. If proxying is used then the <code>find</code> method will |
| sometimes run in a transaction and sometimes not. On the other hand, if weaving is used then the |
| <code>find</code> method will *always* run under a transaction This may seem innocuous, but it |
| can cause big problems. We always want to be certain about where a transaction will start and stop! |
| |
| #### Extending the Simple Example |
| |
| The following type is part of the same project as the previous example, and interacts with it. |
| |
| AbstractDataManager { |
| |
| @Transactional(propagation = NOT_SUPPORTED, readOnly = true) |
| public UniWorksSuperDTO search(Object pk, Object params) { |
| |
| UniWorksSuperDBEntity entity = persistenceDataManager |
| .search(getEntityClass(), createNewPKFromDTOPK(pk), null); |
| |
| loadLinkedTablesTop(entity, params); |
| } |
| |
| protected final void loadLinkedTablesTop(UniWorksSuperDBEntity entity, Object params) { |
| loadLinkedTables(entity, params); |
| } |
| |
| @Transactional(readOnly = true) |
| public void loadLinkedTables(UniWorksSuperDBEntity entity, Object params) { |
| loadLinkedTablesGenerated(entity, params); |
| loadLinkedTransients(entity, params); |
| } |
| } |
| |
| Now lets imagine that a call comes in to the <code>search</code> method of this class from some client. |
| |
| ##### Proxied |
| |
| 1. The container proxy for the data manager halts any ongoing transaction due to the |
| <code>NOT_SUPPORTED</code> metadata, entering an undefined scope. |
| 2. The code calls search on the persistenceDataManager |
| 3. The container proxy for the persistence data manager does not start or stop a transaction due to the |
| <code>SUPPORTS</code> scope. |
| 4. The code calls <code>find</code> on the persistenceDataManager, but without touching the proxy. |
| 5. The code accesses the entity outside a transaction |
| 6. The code returns the entity, and no transaction change is necessary |
| 7. The code calls loadLinkedTablesTop, which calls loadLinkedTables. No proxy is touched therefore no |
| transaction is started. |
| 8. The Tables are populated with data from the entity. Lazy loading is possible as the entity is still attached. |
| |
| ##### Woven |
| |
| 1. The weaving code for the data manager halts any ongoing transaction due to the |
| <code>NOT_SUPPORTED</code> metadata, entering an undefined scope. |
| 2. The code calls search on the persistenceDataManager |
| 3. The weaving code for the persistence data manager does not start or stop a transaction due to the |
| <code>SUPPORTS</code> scope. |
| 4. The code calls <code>find</code> on the persistenceDataManager, at this point the weaving code starts |
| a transaction. |
| 5. The code accesses the entity inside the transaction |
| 6. The code returns the entity, and the transaction completes. This detaches the entity and prevents lazy loading. |
| 7. The code calls loadLinkedTablesTop, which calls loadLinkedTables. The weaving code starts a new |
| transaction. |
| 8. The Code fails as the entity is not able to access its lazily loaded data. |
| |
| ### Strategies for Managing Transaction States |
| |
| Ensuring consistency is vital when writing code that uses transactions. It is therefore usually a good idea to |
| ensure that any reused code is captured in a private method, and that it asserts the correct transaction state |
| before it begins. |
| |
| AbstractDataManager { |
| |
| public UniWorksSuperDTO search(Object pk, Object params) { |
| |
| txControl.build().readOnly().notSupported(() -> { |
| UniWorksSuperDBEntity entity = persistenceDataManager |
| .search(getEntityClass(), createNewPKFromDTOPK(pk), null); |
| |
| loadLinkedTablesTop(entity, params); |
| return entity; |
| } |
| } |
| |
| protected final void loadLinkedTablesTop(UniWorksSuperDBEntity entity, Object params) { |
| loadLinkedTables(entity, params); |
| } |
| |
| public void loadLinkedTables(UniWorksSuperDBEntity entity, Object params) { |
| txControl.build().readOnly().required(() -> { |
| loadLinkedTables(entity, params); |
| }); |
| } |
| |
| /** |
| * This method does not need a transaction, but does need a scope |
| */ |
| private void loadLinkedTablesInternal(UniWorksSuperDBEntity entity, Object params) { |
| assert txControl.activeScope(); |
| |
| loadLinkedTablesGenerated(entity, params); |
| loadLinkedTransients(entity, params); |
| } |
| } |
| |
| Writing code in this way ensures that even when a mixture of transactional and non transactional actions are |
| needed, there will always be a consistent expectation of the transaction scope in each method. |
| |
| ## Exception management |
| |
| The final significant difference between declarative models and the Transaction Control Service is in how much |
| work your application code needs to do when managing exceptions. More detail about managing exceptions |
| [is available here][1]. |
| |
| |
| [1]: exceptionManagement.html |