-
Notifications
You must be signed in to change notification settings - Fork 38k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Inherited @Transactional methods should be able to use class-level TransactionManager qualifier from concrete class #24291
Comments
I couldn't find a reliable way to work around (e.g. defining an own TransactionInterceptor was practically impossible for me due to many unclear interdependencies in Spring code; would be glad to see a working example though). Therefore I don't see any other way as to make changes to the Spring codebase, to the place where transaction attributes are computed. It's all about the class
Unfortunately both implementation have diverged a little. I see some refactorings done on the method in the spring-tx, which were not merged into spring-data-commons. So I will concentrate on the spring-tx implementation for now.
old version: Lines 167 to 168 in c8ef49c
new version: // Second try is the transaction attribute on the target class.
txAttr = findTransactionAttribute(targetClass); Update 2020-03-10: Now I think that the "declaring class" is correct and rather the javadoc/comments should be fixed than the code. But the "targetClass" could/should be considered as well as the next step.
old version: Lines 161 to 186 in c8ef49c
new version: // First try is the method in the target class.
TransactionAttribute txAttr = findTransactionAttribute(specificMethod);
// Second try is the transaction attribute on the target class.
txAttr = merge(txAttr, findTransactionAttribute(targetClass));
if (specificMethod != method) {
// Fallback is to look at the original method.
txAttr = merge(txAttr, findTransactionAttribute(method));
// Last fallback is the class of the original method.
txAttr = merge(txAttr, findTransactionAttribute(method.getDeclaringClass()));
}
return txAttr;
the /** Set empty properties of "primary" object from "secondary" object. */
@Nullable
private TransactionAttribute merge(@Nullable TransactionAttribute primary, @Nullable TransactionAttribute secondary) {
if (primary == null) return secondary;
if (secondary == null) return primary;
if (primary instanceof DefaultTransactionAttribute && secondary instanceof DefaultTransactionAttribute) {
DefaultTransactionAttribute p = (DefaultTransactionAttribute) primary;
DefaultTransactionAttribute s = (DefaultTransactionAttribute) secondary;
if (p.getQualifier() == null) p.setQualifier(s.getQualifier());
if (p.getDescriptor() == null) p.setDescriptor(s.getDescriptor());
if (p.getName() == null) p.setName(s.getName());
// other DefaultTransactionAttribute properties are always set (implicitly or explicitly):
// propagationBehavior = PROPAGATION_REQUIRED, isolationLevel = ISOLATION_DEFAULT, timeout = TIMEOUT_DEFAULT, readOnly = false
}
if (primary instanceof RuleBasedTransactionAttribute && secondary instanceof RuleBasedTransactionAttribute) {
RuleBasedTransactionAttribute p = (RuleBasedTransactionAttribute) primary;
RuleBasedTransactionAttribute s = (RuleBasedTransactionAttribute) secondary;
if (p.getRollbackRules() == null) p.setRollbackRules(s.getRollbackRules());
}
return primary;
} The whole should ideally be refactored to take a list of suppliers of annotation-places (method in target class, target class, method in declaring class, declaring class), ordered by priority, as parameters. Or just one parameter, defining what has a higher priority: annotations in the declaring class (case for spring-data, as this is the user defined interface of the generated target class) or annotations in the target class (general case for all user-defined classes). Such an implementation could then be reused in both Spring projects: spring-data (which requires another priorities, see #17837) and spring-tx. This call from spring-data implementation // Ignore CGLIB subclasses - introspect the actual user class.
Class<?> userClass = ProxyUtils.getUserClass(targetClass); should probably be added as well ( Possibly related issue about Unfortunately I currently have some troubles with setting up a development environment for Spring projects. Could someone try/review the proposed changes and give a feedback? |
OK, I slightly improved the
(patches for 5.2.1 and 5.2.2 are available there as well)
Usage with Maven: Inspect the dependency hierarchy in your project, identify the root Spring artifact which triggers the dependency on <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- spring-tx 5.2.3.RELEASE is used by SpringBoot 2.2.4.RELEASE: -->
<dependency>
<groupId>com.labun</groupId>
<artifactId>spring-tx</artifactId>
<version>5.2.3.RELEASE.patched</version>
</dependency> By enabling transactions related logging
you can see the lines like (printed by
and control that the attributes are as required. You can also enable trace logging just for the
This works well for me so far for the persistence setup described at the beginning, i.e. user-defined DAO/service classes, no Spring Data interface based generated proxies. A solution for Spring Data has to be implemented yet. Feedback is welcome :) And here is the new /** Set empty properties of "primary" object from "secondary" object. */
@Nullable
private TransactionAttribute merge(@Nullable TransactionAttribute primary, @Nullable TransactionAttribute secondary) {
if (primary == null) return secondary;
if (secondary == null) return primary;
if (primary instanceof DefaultTransactionAttribute && secondary instanceof DefaultTransactionAttribute) {
DefaultTransactionAttribute p = (DefaultTransactionAttribute) primary;
DefaultTransactionAttribute s = (DefaultTransactionAttribute) secondary;
if (p.getQualifier() == null || p.getQualifier().isEmpty()) p.setQualifier(s.getQualifier());
if (p.getDescriptor() == null || p.getDescriptor().isEmpty()) p.setDescriptor(s.getDescriptor());
if (p.getName() == null || p.getName().isEmpty()) p.setName(s.getName());
// the following properties have default values in DefaultTransactionDefinition;
// we cannot distinguish here, whether these values have been set explicitly or implicitly;
// but it seems to be logical to handle default values like empty values
if (p.getPropagationBehavior() == PROPAGATION_REQUIRED) p.setPropagationBehavior(s.getPropagationBehavior());
if (p.getIsolationLevel() == ISOLATION_DEFAULT) p.setIsolationLevel(s.getIsolationLevel());
if (p.getTimeout() == TIMEOUT_DEFAULT) p.setTimeout(s.getTimeout());
if (p.isReadOnly() == false) p.setReadOnly(s.isReadOnly());
}
if (primary instanceof RuleBasedTransactionAttribute && secondary instanceof RuleBasedTransactionAttribute) {
RuleBasedTransactionAttribute p = (RuleBasedTransactionAttribute) primary;
RuleBasedTransactionAttribute s = (RuleBasedTransactionAttribute) secondary;
if (p.getRollbackRules() == null || p.getRollbackRules().isEmpty()) p.setRollbackRules(s.getRollbackRules());
}
return primary;
} |
As I had troubles to setup IDE for the (whole) spring-framework project, the patch has been created directly from the spring-tx maven artifact sources. So, unfortunately, cannot currently do a pull request and a unit test :( |
Have you followed the instructions outlined in Building from Source? |
@sbrannen Obviously, not enough... I will certainly give it another try, when I'll have more spare time (perhaps, second half of January). |
OK. The instructions should be correct for both IntelliJ and Eclipse. I personally updated the Eclipse instructions very recently. So if you run into instructions that do not work, please let us know so that we can update the documentation. |
(as described in comment and class javadoc) See spring-projectsgh-24291
Merge transactional attributes from all relevant definition places, according to their priority (instead of returning the first found one). See spring-projectsgh-24291
The PR with patch and additional unit test for the initial use case is created. I also added the "declaring class of the specific method" again to "priorities" (as it was The new priorities are:
As the new ("merging") behavior is not backward compatible, we would need some parameter to switch the implementation (TODO), e.g.
|
spring-projects#24291 : - base DAO class - concrete DAO classes, which declare different transaction managers and inherit transactional methods from the base DAO class
I finally managed to solve the problem without patching the spring-tx library (now, no need to create a patch for every new version of Spring): The default "transactionAttributeSource" bean (defined in ProxyTransactionManagementConfiguration) will be replaced (1) by an instance of the own MergeAnnotationTransactionAttributeSource (2). (1) AnnotationTransactionAttributeSourceReplacer An important part was to implement the import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessor;
import org.springframework.core.PriorityOrdered;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration;
import org.springframework.transaction.interceptor.TransactionAttributeSource;
import lombok.extern.slf4j.Slf4j;
/**
* Replaces the default "transactionAttributeSource" bean (defined in {@link ProxyTransactionManagementConfiguration})
* with instance of {@link MergeAnnotationTransactionAttributeSource}.
*
* @author Eugen Labun
*/
@Slf4j
@Component
public class AnnotationTransactionAttributeSourceReplacer implements InstantiationAwareBeanPostProcessor, PriorityOrdered /*this is important*/ {
public AnnotationTransactionAttributeSourceReplacer() {
// to check that the replacer is created before instantiation of the "transactionAttributeSource" bean
log.trace("AnnotationTransactionAttributeSourceReplacer - constructor");
}
@Override
public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException {
// log.trace("postProcessBeforeInstantiation - beanName: {}, beanClass: {}", beanName, beanClass);
if (beanName.equals("transactionAttributeSource") && TransactionAttributeSource.class.isAssignableFrom(beanClass)) {
log.debug("instantiating bean {} as {}", beanName, MergeAnnotationTransactionAttributeSource.class.getName());
return new MergeAnnotationTransactionAttributeSource();
} else {
return null;
}
}
@Override
public int getOrder() {
return 0;
}
} (2) MergeAnnotationTransactionAttributeSource import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import org.springframework.aop.support.AopUtils;
import org.springframework.lang.Nullable;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.annotation.AnnotationTransactionAttributeSource;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.interceptor.AbstractFallbackTransactionAttributeSource;
import org.springframework.transaction.interceptor.DefaultTransactionAttribute;
import org.springframework.transaction.interceptor.RuleBasedTransactionAttribute;
import org.springframework.transaction.interceptor.TransactionAttribute;
import org.springframework.util.ClassUtils;
import lombok.extern.slf4j.Slf4j;
/**
* Implements a merge policy for transaction attributes (see {@link Transactional} annotation)
* with following priorities (high to low):
* <ol>
* <li>specific method;
* <li>declaring class of the specific method;
* <li>target class;
* <li>method in the declaring class/interface;
* <li>declaring class/interface.
* </ol>
*
* <p>The merge policy means that all transaction attributes which are not
* explicitly set [1] on a specific definition place (see above) will be inherited
* from the place with the next lower priority.
*
* <p>On the contrary, the Spring default {@link AbstractFallbackTransactionAttributeSource} implements a fallback policy,
* where all attributes are read from the first found definition place (essentially in the above order), and all others are ignored.
*
* <p>See analysis in <a href="https://github.com/spring-projects/spring-framework/issues/24291">Inherited @Transactional methods use wrong TransactionManager</a>.
*
* <p>[1] If the value of an attribute is equal to its default value, the current implementation
* cannot distinguish, whether this value has been set explicitly or implicitly,
* and considers such attribute as "not explicitly set". Therefore it's currently impossible to override a non-default value with a default value.
*
* @author Eugen Labun
*/
@Slf4j
@SuppressWarnings("serial")
public class MergeAnnotationTransactionAttributeSource extends AnnotationTransactionAttributeSource {
public MergeAnnotationTransactionAttributeSource() {
log.info("MergeAnnotationTransactionAttributeSource constructor");
}
@Override
@Nullable
protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass) {
// Don't allow no-public methods as required.
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
// The method may be on an interface, but we also need attributes from the target class.
// If the target class is null, the method will be unchanged.
Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);
// 1st priority is the specific method.
TransactionAttribute txAttr = findTransactionAttribute(specificMethod);
// 2nd priority is the declaring class of the specific method.
Class<?> declaringClass = specificMethod.getDeclaringClass();
boolean userLevelMethod = ClassUtils.isUserLevelMethod(method);
if (userLevelMethod) {
txAttr = merge(txAttr, findTransactionAttribute(declaringClass));
}
// 3rd priority is the target class
if (targetClass != null && !targetClass.equals(declaringClass) && userLevelMethod) {
txAttr = merge(txAttr, findTransactionAttribute(targetClass));
}
if (method != specificMethod) {
// 4th priority is the method in the declaring class/interface.
txAttr = merge(txAttr, findTransactionAttribute(method));
// 5th priority is the declaring class/interface.
txAttr = merge(txAttr, findTransactionAttribute(method.getDeclaringClass()));
}
return txAttr;
}
/**
* Set empty and default properties of "primary" object from "secondary" object.
* <p>Parameter objects should not be used after the call to this method,
* as they can be changed here or/and returned as a result.
*/
@Nullable
private TransactionAttribute merge(@Nullable TransactionAttribute primaryObj, @Nullable TransactionAttribute secondaryObj) {
if (primaryObj == null) {
return secondaryObj;
}
if (secondaryObj == null) {
return primaryObj;
}
if (primaryObj instanceof DefaultTransactionAttribute && secondaryObj instanceof DefaultTransactionAttribute) {
DefaultTransactionAttribute primary = (DefaultTransactionAttribute) primaryObj;
DefaultTransactionAttribute secondary = (DefaultTransactionAttribute) secondaryObj;
if (primary.getQualifier() == null || primary.getQualifier().isEmpty()) {
primary.setQualifier(secondary.getQualifier());
}
if (primary.getDescriptor() == null || primary.getDescriptor().isEmpty()) {
primary.setDescriptor(secondary.getDescriptor());
}
if (primary.getName() == null || primary.getName().isEmpty()) {
primary.setName(secondary.getName());
}
// The following properties have default values in DefaultTransactionDefinition;
// we cannot distinguish here, whether these values have been set explicitly or implicitly;
// but it seems to be logical to handle default values like empty values.
if (primary.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED) {
primary.setPropagationBehavior(secondary.getPropagationBehavior());
}
if (primary.getIsolationLevel() == TransactionDefinition.ISOLATION_DEFAULT) {
primary.setIsolationLevel(secondary.getIsolationLevel());
}
if (primary.getTimeout() == TransactionDefinition.TIMEOUT_DEFAULT) {
primary.setTimeout(secondary.getTimeout());
}
if (!primary.isReadOnly()) {
primary.setReadOnly(secondary.isReadOnly());
}
}
if (primaryObj instanceof RuleBasedTransactionAttribute && secondaryObj instanceof RuleBasedTransactionAttribute) {
RuleBasedTransactionAttribute primary = (RuleBasedTransactionAttribute) primaryObj;
RuleBasedTransactionAttribute secondary = (RuleBasedTransactionAttribute) secondaryObj;
if (primary.getRollbackRules() == null || primary.getRollbackRules().isEmpty()) {
primary.setRollbackRules(secondary.getRollbackRules());
}
}
return primaryObj;
}
} |
Hello. I found this issue, because I have met strange behaviour. I just have a simple structure
And when I invoke 'method' on instance 'B', I still get transactionAttributes from the super class. @elab Did you continue work on PR? Did you think about rewriting primary transaction attributes by any override (In your code it is secondaryObj). |
Could you give an example? |
Yes, of course. I will improve the example Fistful DAO structures:
And I invokes read method on the MasterRepository instance: |
OK, I understand your thought. What about
(I removed Which value of TransactionAttribute.readOnly would you expect now for masterRepository.read() ? |
I think it still should be readOnly = false behaviour, because it is default value for annotation. |
@kurenchuksergey Sorry for the delay. The "override" behavior you wrote about is essentially how the standard Spring library works: The Note, that the highest priority, according to the logic in On the contrary, the |
It looks like we'll address both this and #23473 through a new |
@Transactional
methods should be able to use class-level TransactionManager
@Transactional
methods should be able to use class-level TransactionManager
As a rather design-oriented measure, we are going to take type-level Such a type-level bean qualifier can serve multiple purposes, e.g. with a value of "accountdb" it can be used for autowiring purposes (identifying the account repository) as well as transaction manager selection, as long as the target beans for autowiring as well as the associated transaction manager definitions declare the same qualifier value. While this technically also works against target bean names, it is recommended to use custom We do not recommend any use of type-level |
Setup: In a multi-DB environment, there is an abstract Service/DAO class with
@Transactional
methods common to all entities from all DBs. Then there are multiple concrete Service/DAO beans, one per DB, each with specific transactionManager in the class-level@Transactional
annotation.Problem: When calling a base-class method via some DB-specific bean, a wrong transactionManager is used (always the
@Primary
one). The worst is that no exceptions are thrown, no error messages printed in log. The entities are simply silently e.g. not saved/updated in DB. Or, I afraid, they could even be saved to wrong DB.The problem has already been described at least in #14011, #14295, #17080. Related issue is also #11839. But all the issues have been closed (as outdated) without a solution.
There were also some workarounds: https://stackoverflow.com/questions/51087660/dynamic-selection-of-transactionmanager-spring-boot, https://www.tirasa.net/en/blog/dynamic-springs-at-transactional, also #14295. But they all are not working (anymore).
How can it be solved? What do you, guys, do in such situation? Am I missing something obvious?
Thank you for your thoughts.
The text was updated successfully, but these errors were encountered: