Transaction managment in Play2


#1

Hi,

I try to migrate a Play1 application in Play2 (version 2.6.15 at the moment).

With Play1, transactions were automatically managed when a request arrived. So I didn’t have to worry about consistency of my database updates. Database updates were done all over the service layer.

I have to keep that logic in Play2 and I’m not sure how to do that.

To understand how it works, I worked on a snippet with 2 twos models : Person and Address. I want to save a person and the list of their addresses and rollback in case an error occurs on one of the requests.

To start my migration, I was inspired by play-java-jpa-example : I created some repositories that contain all access to database and executed on DatabaseExecutionContext

public class PersonRepository implements IPersonRepository {

    private final JPAApi jpaApi;
    private final DatabaseExecutionContext executionContext;

    @Inject
    public PersonRepository(JPAApi jpaApi, DatabaseExecutionContext executionContext) {
        this.jpaApi = jpaApi;
        this.executionContext = executionContext;
    }
    ...
    @Override
    public CompletionStage<Person> add(Person person) {
        return supplyAsync(() -> jpaApi.withTransaction(em -> {
            em.persist(person);
            return person;
        }), executionContext);
    }
}
public class AddressRepository implements IAddressRepository {
    ...
    @Override
    public CompletionStage<Address> add(Address address) {
        return supplyAsync(() -> jpaApi.withTransaction(em -> {
            em.persist(address);
            return address;
        }), executionContext);
    }
}

I have a service to save Address

public class AddressService implements IAddressService {
    ....
    @Override
    public CompletionStage<Address> add(Address address) {
        //do some stuff here
        return addressRepository.add(address);
    }
}

And a service to save a Person, with

public class PersonService implements IPersonService {
    ...
    @Override
    public CompletionStage<Person> add(Person person, List<Address> address) {
        return add(person).thenApplyAsync(p -> {
            for (Address a : address) {
                a.person=p;
                addressServiceAsync.add(a);
            }
            return p;
        }, ec.current());
    }
}

With this implementation of add(Person person, List<Address> address), if saving the second address fails, the person and the first address will have been persisted in the database which is not good enough for me.

I tried to remove transaction management in repositories and put it in my PersonService.add function. By passing the entity manager to the functions of my services and repositories it works (I only tested with synchronous calls). Something like that :

public class PersonService implements IPersonService {
    @Override
    public CompletionStage<Person> add(Person person, List<Address> address) {
        return supplyAsync(() -> jpaApi.withTransaction(em -> {
            Person person1 = personRepository.insert(em, person);
            for (Address a : address) {
                a.person = person1;
                addressService.add(em, a);
            }
            return person1;
        })).exceptionally(throwable -> {
            Logger.error("pan", throwable);
            return null;
        });
    }
}

I don’t like the approach (giving em to all functions) and wonder about asynchronous calls.

What is planned in Play to handle this kind of rollback problem with JPAApi and DatabaseExecutionContext ?

I didn’t see any explicit thread evoking this point, maybe I missed something. What would be the best practice to solve this problem?

Thanks for your help.