Transaction Management

Jmix supports both standard ways to control transactions in Spring applications: declarative (with annotations) and programmatic.

Declarative Transaction Management

The most straightforward way of transaction management in Jmix applications is to use the @org.springframework.transaction.annotation.Transactional annotation. This annotation indicates a method that should run inside a database transaction. When used on the class level, @Transactional applies to all methods of this class and its subclasses.

The @Transactional annotation will automatically create a transaction when the method is called, and the commit or rollback will be managed implicitly by Spring. Thus, the declarative transaction management allows you to reduce the amount of boilerplate code.

A number of parameters can be used to refine the @Transactional behavior, for example, the isolation level or propagation: these parameters are described in the Spring documentation.

Example of @Transactional usage for updating multiple entities within a single transaction:

@Transactional (1)
public void makeDiscountsForAll() {
    List<Order> orders = dataManager.load(Order.class)
            .query("select o from sample_Order o where o.customer is not null")
            .list();
    for (Order order : orders) {
        BigDecimal newTotal = orderService.calculateDiscount(order);
        order.setAmount(newTotal);
        dataManager.save(order);
        Customer customer = customerService.updateCustomerGrade(order.getCustomer());
        dataManager.save(customer);
    }
}
1 You simply put the annotation, Spring does all the rest: the proxy will be created to inject the transactional logic before (starting transaction) and after (committing or rolling back) the running method.
Keep in mind that declarative markup works only if the method is called on an instance injected to another bean or obtained by ApplicationContext.getBean(), that is through the proxy created by the container. Calling an annotated method from another method of the same object will not start a transaction.

If you need to declare a transaction for an additional data store, specify the name of the data store’s transaction manager bean in the @Transactional annotation. If you created the data store using Studio, its transaction manager’s bean name is <DATA-STORE-NAME>TransactionManager. For example, if the data store name is db1, a transactional method for it is defined as follows:

@Transactional("db1TransactionManager")
public void makeChangesInDb1DataStore() {
    // ...
}

Programmatic Transaction Management

For programmatic transaction management, Spring offers the org.springframework.transaction.support.TransactionTemplate class.

Creating TransactionTemplate

To create an instance of TransactionTemplate, you may declare a bean in the main application class (annotated with @SpringBootApplication) and initialize it with a PlatformTransactionManager:

@Bean
@Primary
TransactionTemplate transactionTemplate(PlatformTransactionManager transactionManager) {
    return new TransactionTemplate(transactionManager);
}

Now you can inject TransactionTemplate in any bean of your application:

@Autowired
private TransactionTemplate transactionTemplate;

Additional data stores, if any, require their own TransactionTemplate instances. If you create an additional data store with Studio, the data store’s Spring configuration class will be created automatically with some beans in it. Add a new one for creating TransactionTemplate with the Qualifier annotation in the following way:

@Bean
TransactionTemplate db1TransactionTemplate(
        @Qualifier("db1TransactionManager") (1)
                PlatformTransactionManager transactionManager) {
    return new TransactionTemplate(transactionManager);
}
1 The @Qualifier annotation is used for injecting a particular bean by its name: in this case, it’s the PlatformTransactionManager defined for the additional data store named db1.

So, you can inject the required TransactionTemplate with the Qualifier annotation to manage transactions in the additional data store:

@Autowired
@Qualifier("db1TransactionTemplate") (1)
private TransactionTemplate db1TransactionTemplate;
1 Here, the @Qualifier annotation allows Spring to select the bean that we defined above for the db1 data store.

If you don’t need TransactionTemplate to be available everywhere in the project, you can create it locally in a bean using PlatformTransactionManager. The example below shows creation of two templates with different propagation behavior:

@Autowired
private PlatformTransactionManager transactionManager;

// joins existing transaction
public TransactionTemplate getTransactionTemplate() {
    return new TransactionTemplate(transactionManager);
}

// always creates new transaction
public TransactionTemplate getRequiresNewTransactionTemplate() {
    TransactionTemplate tt = new TransactionTemplate(transactionManager);
    tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
    return tt;
}

Using TransactionTemplate

Use the execute() method to run a block of code inside a transaction. This method handles the transaction lifecycle and possible exceptions, so you don’t need to handle them explicitly:

public UUID createOrderAndReturnId() {
    return transactionTemplate.execute(status -> {
        Customer customer = dataManager.create(Customer.class);
        customer.setName("Alice");
        customer = dataManager.save(customer);

        Order order = dataManager.create(Order.class);
        order.setCustomer(customer);

        order = dataManager.save(order);
        return order.getId();
    });
}

If you don’t need to return any result from the transactional code block, you can use the executeWithoutResult() method which is derived from execute() but uses the TransactionCallbackWithoutResult callback interface:

public void createOrder() {
    transactionTemplate.executeWithoutResult(status -> {
        Customer customer = dataManager.create(Customer.class);
        customer.setName("Alice");
        customer = dataManager.save(customer);

        Order order = dataManager.create(Order.class);
        order.setCustomer(customer);

        dataManager.save(order);
    });
}

The default transaction settings, such as the propagation mode, the isolation level, the timeout etc., can be customized with the TransactionTemplate setters.