web-dev-qa-db-ja.com

@Asyncでネストされた@Transactionalメソッド

SpringとJPAを使用しています。 @EnableAsync@EnableTransactionManagementをオンにしました。ユーザー登録サービスメソッドには、@Asyncという注釈が付けられた他のいくつかのサービスメソッドがあります。これらの方法は、ウェルカムメールの送信や、新しく作成されたユーザーのサードパーティ決済システムへの登録など、さまざまなことを行います。

サードパーティの支払いシステムがユーザーを正常に作成したことを確認するまで、すべてが正常に機能します。その時点で、@AsyncメソッドはUserAccount(新しく作成されたUserを参照)を作成しようとし、javax.persistence.EntityNotFoundException: Unable to find com.dk.st.model.User with id 2017でエラーになります。

登録呼び出しは次のようになります。

private User registerUser(User newUser, Boolean waitForAccount) {
    String username = newUser.getUsername();
    String email = newUser.getEmail();

    // ... Verify the user doesn't already exist

    // I have tried all manner of flushing and committing right here, nothing works
    newUser = userDAO.merge(newUser);

    // Here is where we register the new user with the payment system.
    // The User we just merged is not /actually/ in the DB
    Future<Customer> newCustomer = paymentService.initializeForNewUser(newUser);
    // Here is where I occasionally (in test methods) pause this thread to wait
    // for the successful account creation.
    if (waitForAccount) {
        try {
            newCustomer.get();
        } catch (Exception e) {
            logger.error("Exception while creating user account!", e);
        }
    }

    // Do some other things that may or may not be @Aysnc

    return newUser;
}

支払いサービスは、ユーザーを登録する作業を行うように呼び出し、次のようになります。

@Async
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public Future<Customer> initializeForNewUser(User newUser) {
    // ... Set up customerParams

    Customer newCustomer = null;
    try {
        newCustomer = Customer.create(customerParams);

        UserAccount newAccount = new UserAccount();
        newAccount.setUser(newUser);
        newAccount.setCustomerId(newCustomer.getId());
        newAccount.setStatus(AccountStatus.PRE_TRIAL);

        // When merging, JPA cannot find the newUser object in the DB and complains
        userAccountDAO.merge(newAccount);
    } catch (Exception e) {
        logger.error("Error while creating UserAccount!", e);
        throw e;
    }

    return new AsyncResult<Customer>(newCustomer);
}

リストされているStackOverflowの回答 ここ は、私が行ったREQUIRES_NEW伝播を設定したことを示唆していますが、そのような運はありません。

誰かが私を正しい方向に向けることができますか?コントローラーメソッドから直接paymentServiceを呼び出す必要は本当にありません。確かにサービスレベルの呼びかけだと思います。

助けてくれてありがとう!

11
Michael Ressler

Vyncentの助けを借りて、これが私が到達した解決策です。 UserCreationServiceという新しいクラスを作成し、Userの作成を処理するすべてのメソッドをそのクラスに配置しました。次に例を示します。

@Override
public User registerUserWithProfileData(User newUser, String password, Boolean waitForAccount) {
    newUser.setPassword(password);
    newUser.encodePassword();
    newUser.setJoinDate(Calendar.getInstance(TimeZone.getTimeZone("UTC")).getTime());

    User registered = userService.createUser(newUser);
    registered = userService.processNewRegistration(registered, waitForAccount);

    return userService.setProfileInformation(registered);
}

このメソッドには[〜#〜] no [〜#〜]@Transactionalアノテーションがあることに気付くでしょう。これは意図的なものです。対応するcreateUserおよびprocessNewRegistrationの定義は次のようになります。

@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public User createUser(User newUser) {
    String username = newUser.getUsername();
    String email = newUser.getEmail();

    if ((username != null) && (userDAO.getUserByUsername(username) != null)) {
        throw new EntityAlreadyExistsException("User already registered: " + username);
    }

    if (userDAO.getUserByUsername(newUser.getEmail()) != null) {
        throw new EntityAlreadyExistsException("User already registered: " + email);
    }

    return userDAO.merge(newUser);
}

@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public User processNewRegistration(
        User newUser,
        Boolean waitForAccount) 
{
    Future<UserAccount> customer = paymentService.initializeForNewUser(newUser);
    if (waitForAccount) {
        try {
            customer.get();
        } catch (Exception e) {
            logger.error("Error while creating Customer object!", e);
        }
    }

    // Do some other maintenance type things...

    return newUser;
}

Vyncentは、トランザクション管理が問題であることに気づきました。他のサービスを作成することで、これらのトランザクションがいつコミットされるかをより適切に制御できるようになりました。私は最初はこのアプローチを取ることを躊躇していましたが、それはSpringが管理するトランザクションとプロキシとのトレードオフです。

これが他の誰かが後で時間を節約するのに役立つことを願っています。

9
Michael Ressler

新しいserServiceクラスを作成して、ユーザーチェックを管理してみてください。

@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public User createOrUpdateUser(User newUser) {
    String username = newUser.getUsername();
    String email = newUser.getEmail();

    // ... Verify the user doesn't already exist

    // I have tried all manner of flushing and committing right here, nothing works
    newUser = userDAO.merge(newUser);
    return newUser;
}

次に、実際のクラスで変更します

private User registerUser(User newUser, Boolean waitForAccount) {
    String username = newUser.getUsername();
    String email = newUser.getEmail();

    // ... Verify the user doesn't already exist

    // I have tried all manner of flushing and committing right here, nothing works
    newUser = userDAO.merge(newUser);

沿って

private User registerUser(User newUser, Boolean waitForAccount) {
    newUser = userService.createOrUpdateUser(newUser);

@ Transactional REQUIRES_NEWの新しいuserServiceは、コミットを強制し、問題を解決する必要があります。

4
Vyncent