0

My Spring Boot app has the following classes:

Board (JPA entity)

@Entity
@Table(name = "board")
public class Board {
  public static final int IN_PROGRESS = 1;
  public static final int AFK         = 2;
  public static final int COMPLETED   = 3;

  @Column(name = "id")
  @Generated(GenerationTime.INSERT)
  @GeneratedValue(strategy = GenerationType.AUTO)
  @Id
  private Long id;

  @Column(name = "status", nullable = false)
  private int status = IN_PROGRESS;
}

BoardRepository (JPA repository)

public interface BoardRepository extends JpaRepository<Board, Long> {}

CommonBoardService (base service)

public interface CommonBoardService {
  Board save(Board board);
  Board update(Board board, int status);
}

CommonBoardServiceImpl (base service implementation)

@Service
@Transactional
public class CommonBoardServiceImpl implements CommonBoardService {
  @Autowired
  private BoardRepository boardRepository;

  public Board save(final Board board) {
    return boardRepository.save(board);
  }

  @Transactional(propagation = Propagation.REQUIRES_NEW)
  public Board update(final Board board, final int status) {
    board.setStatus(status);

    return save(board);
  }
}

BoardService (specific service interface)

public interface BoardService {
  Board startBoard();
  void synchronizeBoardState(Board board);
}

BoardServiceImpl (specific service implementation)

@Service
@Transactional
public class BoardServiceImpl implements BoardService {
  @Autowired
  private CommonBoardService commonBoardService;

  public Board startBoard() { return new Board(); }

  public void synchronizeBoardState(final Board board) {
    if (board != null && inProgress(board)) {
      if (!canPlayWithCurrentBoard(board)) {
        commonBoardService.update(board, Board.AFK);
      }
      else {
        commonBoardService.update(board, Board.COMPLETED);
      }
    }
  }

  private boolean canPlayWithCurrentBoard(final Board board) {
    return !inProgress(board);
  }

  private boolean inProgress(final Board board) {
    return board != null && board.getStatus() == Board.IN_PROGRESS;
  }
}

BoardServiceTest (unit test)

1.  @RunWith(SpringJUnit4ClassRunner.class)
2.  @Transactional
3.  public class BoardServiceTest {
4.    @Autowired
5.    private BoardRepository boardRepository;
6.
7.    @Autowired
8.    private BoardService       boardService;
9.    @Autowired
10.   private CommonBoardService commonBoardService;
11.
12.   @Test
13.   public void testSynchronizeBoardStatus() {
14.     Board board = boardService.startBoard();
15.
16.     board = commonBoardService.save(board);
17.
18.     assertEquals(1, boardRepository.count());
19.
20.     boardService.synchronizeBoardState(board);
21.
22.     assertEquals(1, boardRepository.count());
23.   }
24. }

This test fails on line 22 with the error java.lang.AssertionError: Expected :1 Actual:2. Hibernate SQL logs reveal an INSERT being fired on line 20 instead of an UPDATE. Since I am using the same Board object throughout the test, I expect line 20 to fire an UPDATE instead of an INSERT.

Can anyone explain why this is happening and how to get the expected behaviour (UPDATE on line 20)?

manish
  • 19,695
  • 5
  • 67
  • 91
Hutsul
  • 1,535
  • 4
  • 31
  • 51
  • Which database is your TestDataSourceConfiguration.class pointing to? Does it have the board entity you want to update? – koder23 Jun 09 '16 at 03:52
  • What are `boardService` and `commonBoardService` in `BoardServiceTest`? What do `boardService.startBoard(admin)` and `commonBoardService.save(board)` look like? Presumably, the first is creating a new `board` and the second saving it? Also, what does the code for the `Board` and `User` classes look like? It may be better if you can create a small sample that demonstrates the problem as there is quite a lot of code to cover. – manish Jun 09 '16 at 07:43
  • @koder23, thanks for response. I am using postgres. Yea, it should has as `Board board = boardService.startBoard(admin);` - create the new board, and the ID of the entity is equals to 1. – Hutsul Jun 09 '16 at 08:52
  • Can you post the complete code to test it out? There are still many parts missing that make it difficult to reproduce the problem. May be post a sample app on Github? – manish Jun 09 '16 at 08:58
  • @manish, you are right, `boardService.startBoard(admin)` - create new board, for the user. As you can see, the `boardService.synchronizeBoardState(board, admin)` has different checks. So, I want to trigger the case when we are going inside the `if (!canPlayWithCurrentBoard(board))` code block. To achieve that I need update the `board` entity to met those condition. That is why I use the `commonBoardService.save(board)` in the test. – Hutsul Jun 09 '16 at 09:02
  • @manish, Ok, I will add the rest of the code. If I'll miss some component, please tell me. – Hutsul Jun 09 '16 at 09:05
  • @manish, I have updated the question – Hutsul Jun 09 '16 at 09:15
  • @I.Domshchikov, I have simplified your code as it was overly long. A [sample app](https://github.com/manish-in-java/stackoverflow-questions/tree/master/37698026) is also available on Github. The problem you are facing is reproducible in the sample app. – manish Jun 09 '16 at 09:48
  • @manish, thanks appreciate that. – Hutsul Jun 09 '16 at 09:57
  • Your title includes Spring Boot but I don't see anything Spring Boot related in your setup or testcase... – M. Deinum Jun 09 '16 at 10:01
  • @I.Domshchikov, can you try my sample application linked in my answer below and check after remove `Propagation.REQUIRES_NEW`? – manish Jun 09 '16 at 11:19
  • @manish. I have looked into the provided sample and have a comment. The `boardService.startBoard()` method besides creating new entity, also should save it into db. In my impl, it works that way. – Hutsul Jun 10 '16 at 07:50
  • @manish. I couldn't remove the `Propagation.REQUIRES_NEW` as it would change my business logic. If everything would be run in the same transaction, than in case of any exception all transaction will be rollback. – Hutsul Jun 10 '16 at 07:54

1 Answers1

1

The culprit is this line: @Transactional(propagation = Propagation.REQUIRES_NEW). Lets see what happens when the test case is executed.

  • Because BoardServiceTest is annotated with @Transactional a new transaction is started when BoardServiceTest.testSynchronizeBoardStatus starts executing.
  • Line 14 creates a new Board instance.
  • Line 16 attempts to save the Board instance created on line 14 and triggers a database INSERT.
  • Line 20 indirectly invokes CommonBoardServiceImpl.update which is annotated with @Transactional(propagation = Propagation.REQUIRES_NEW). This suspends the ongoing transaction (see the JavaDocs for Propagation), which has neither been committed nor been rolled back so far.
  • CommonBoardServiceImpl.update in turn attempts to save the Board instance passed to it.
  • The given instance is not recognized as an existing instance because the transaction that saved it to the database is currently in suspended state. Hence, it is assumed to be a new instance and results in a second INSERT.
  • Line 20 now finishes, which commits the inner transaction started for CommonBoardServiceImpl.update. The outer transaction resumes.
  • Line 22 finds a dirty session and flushes it before firing a SELECT query. This means there are now two instances in the database, hence the test failure.

Removing @Transactional(propagation = Propagation.REQUIRES_NEW) ensures that the entire test is executed within the same transaction and hence passes.

manish
  • 19,695
  • 5
  • 67
  • 91