无法使用Spring Boot更新现有实体

时间:2016-06-08 09:07:49

标签: spring-data-jpa jpa-2.1

My Spring Boot应用程序包含以下类:

  

董事会(JPA实体)

@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存储库)

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

CommonBoardService (基本服务)

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

CommonBoardServiceImpl (基本服务实施)

@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 (特定服务界面)

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

BoardServiceImpl (特定服务实施)

@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 (单元测试)

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. }

此测试在第22行失败,错误为java.lang.AssertionError: Expected :1 Actual:2。 Hibernate SQL日志显示在第20行而不是INSERT上触发了UPDATE。由于我在整个测试中使用了相同的Board对象,因此我希望第20行能够触发UPDATE而不是INSERT

任何人都可以解释为什么会发生这种情况以及如何获得预期的行为(第20行UPDATE)?

1 个答案:

答案 0 :(得分:1)

罪魁祸首是这一行:@Transactional(propagation = Propagation.REQUIRES_NEW)。让我们看一下测试用例执行时会发生什么。

  • 由于BoardServiceTest注明@Transactional,因此BoardServiceTest.testSynchronizeBoardStatus开始执行时会启动新事务。
  • 第14行创建一个新的Board实例。
  • 第16行尝试保存第14行创建的Board实例并触发数据库INSERT
  • 第20行间接调用CommonBoardServiceImpl.update,注释@Transactional(propagation = Propagation.REQUIRES_NEW)。这将暂停正在进行的事务(请参阅JavaDocs for Propagation),该事务迄今尚未提交或已回滚。
  • CommonBoardServiceImpl.update反过来尝试保存传递给它的Board个实例。
  • 给定实例未被识别为现有实例,因为将其保存到数据库的事务当前处于挂起状态。因此,它被假定为一个新实例,并导致第二个INSERT
  • 第20行现在完成,它将为CommonBoardServiceImpl.update提交内部事务。外部交易恢复。
  • 第22行找到一个脏会话并在触发SELECT查询之前将其刷新。这意味着数据库中现在有两个实例,因此测试失败。

删除@Transactional(propagation = Propagation.REQUIRES_NEW)可确保整个测试在同一事务中执行,从而通过。