Eager
Eager

Reputation: 1691

Existing entity cannot be updated with Spring Boot

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)?

Upvotes: 0

Views: 621

Answers (1)

manish
manish

Reputation: 20135

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.

Upvotes: 1

Related Questions