如何编写一个RestController来从XML请求更新JPA实体,Spring Data JPA方式?

时间:2016-12-25 07:18:58

标签: spring spring-mvc spring-boot spring-data-jpa

我有一个名为person的表的数据库:

 id | first_name | last_name | date_of_birth 
----|------------|-----------|---------------
 1  | Tin        | Tin       | 2000-10-10    

有一个名为Person的JPA实体映射到此表:

@Entity
@XmlRootElement(name = "person")
@XmlAccessorType(NONE)
public class Person {

    @Id
    @GeneratedValue
    private Long id;

    @XmlAttribute(name = "id")
    private Long externalId;

    @XmlAttribute(name = "first-name")
    private String firstName;

    @XmlAttribute(name = "last-name")
    private String lastName;

    @XmlAttribute(name = "dob")
    private String dateOfBirth;

    // setters and getters
}

该实体还使用JAXB注释进行注释,以允许XML有效负载 HTTP请求映射到实体的实例。

我想实现一个端点,用于检索和更新具有给定id的实体。

根据this answer to a similar question, 我需要做的就是按如下方式实现处理程序方法:

@RestController
@RequestMapping(
        path = "/persons",
        consumes = APPLICATION_XML_VALUE,
        produces = APPLICATION_XML_VALUE
)
public class PersonController {

    private final PersonRepository personRepository;

    @Autowired
    public PersonController(final PersonRepository personRepository) {
        this.personRepository = personRepository;
    }

    @PutMapping(value = "/{person}")
    public Person savePerson(@ModelAttribute Person person) {
        return personRepository.save(person);
    }

}

然而,由于以下失败的测试用例可以验证,这无法正常工作:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = RANDOM_PORT)
public class PersonControllerTest {

    @Autowired
    private TestRestTemplate restTemplate;

    private HttpHeaders headers;

    @Before
    public void before() {
        headers = new HttpHeaders();
        headers.setContentType(APPLICATION_XML);
    }

    // Test fails
    @Test
    @DirtiesContext
    public void testSavePerson() {
        final HttpEntity<Object> request = new HttpEntity<>("<person first-name=\"Tin Tin\" last-name=\"Herge\" dob=\"1907-05-22\"></person>", headers);

        final ResponseEntity<Person> response = restTemplate.exchange("/persons/1", PUT, request, Person.class, "1");
        assertThat(response.getStatusCode(), equalTo(OK));

        final Person body = response.getBody();
        assertThat(body.getFirstName(), equalTo("Tin Tin")); // Fails
        assertThat(body.getLastName(), equalTo("Herge"));
        assertThat(body.getDateOfBirth(), equalTo("1907-05-22"));
    }

}

第一个断言失败了:

java.lang.AssertionError: 
Expected: "Tin Tin"
     but: was "Tin"
Expected :Tin Tin
Actual   :Tin

换句话说:

  • 不会发生服务器端异常(状态代码为200
  • Spring使用id=1
  • 成功加载Person实例
  • 但其属性未更新

任何想法我在这里缺少什么?

注1

提供的解决方案here无效。

注2

提供了演示此问题的完整工作代码 here

更多细节

预期行为:

  1. 使用id=1
  2. 加载Person实例
  3. 使用Jaxb2RootElementHttpMessageConverterMappingJackson2XmlHttpMessageConverter
  4. 使用XML有效内容填充加载的人员实体的属性
  5. 将其作为其person参数
  6. 交给控制器的操作处理程序

    实际行为:

    1. 已加载id=1的人员实例
    2. 实例的属性未更新以匹配请求有效内容中的XML
    3. 传递给控制器​​的操作处理程序方法的人员实例的属性未更新

4 个答案:

答案 0 :(得分:3)

这个'@PutMapping(value =“/ {person}”)带来了一些魔力,因为在你的情况下{person}只是'1',但它碰巧从数据库加载它并放到控制器中的ModelAttribute。无论你在测试中改变什么(它甚至可能是空的),spring都会从数据库中加载人员(实际上忽略你的输入),你可以在控制器的第一行停止调试器来验证它。

您可以这样使用它:

@PutMapping(value = "/{id}")
public Person savePerson(@RequestBody Person person, @PathVariable("id") Long id ) {
    Person found = personRepository.findOne(id);

    //merge 'found' from database with send person, or just send it with id
    //Person merged..
    return personRepository.save(merged);
   }

答案 1 :(得分:1)

  1. 控制器中的错误映射
  2. 更新实体,您需要将其置于持久(托管)状态首先,然后在其上复制所需的状态。
  3. 考虑为您的业务对象引入DTO,因为稍后,使用持久化状态实体进行响应可能会导致麻烦(例如,不期望的延迟集合提取或实体关系序列化为XML,JSON可能因无限方法调用而导致堆栈溢出)
  4. 以下是修复测试的简单案例:

    @PutMapping(value = "/{id}")
    public Person savePerson(@PathVariable Long id, @RequestBody Person person) {
        Person persisted = personRepository.findOne(id);
        if (persisted != null) {
            persisted.setFirstName(person.getFirstName());
            persisted.setLastName(person.getLastName());
            persisted.setDateOfBirth(person.getDateOfBirth());
            return persisted;
        } else {
            return personRepository.save(person);
        }
    }
    

    <强>更新

    @PutMapping(value = "/{person}")
    public Person savePerson(@ModelAttribute Person person, @RequestBody Person req) {
        person.setFirstName(req.getFirstName());
        person.setLastName(req.getLastName());
        person.setDateOfBirth(req.getDateOfBirth());
        return person;
    }
    

答案 2 :(得分:0)

问题在于,当您调用personRepository.save(person)时,您的人员实体没有主键字段(id),因此数据库最终会有两条记录,其中新记录主键由数据库生成。修复方法是为id字段创建一个setter,并在保存之前使用它来设置实体的id:

@PutMapping(value = "/{id}") public Person savePerson(@RequestBody Person person, @PathVariable("id") Long id) { person.setId(id); return personRepository.save(person); }

另外,就像@freakman所建议的那样,您应该使用@RequestBody来捕获原始json / xml并将其转换为域模型。此外,如果您不想为主键字段创建一个setter,另一个选项可能是支持基于任何其他唯一字段(如externalId)的更新操作,而是调用它。

答案 3 :(得分:0)

为了更新任何实体,加载和保存必须在同一个Transaction中,否则它将在save()调用中创建新的实体,或者将抛出重复的主键约束违例异常。

更新我们需要将实体,load()/ find()和save()放在同一个事务中,或者在@Repository类中编写JPQL UPDATE查询,并使用@Modifying注释该方法。

@Modifying注释不会触发额外的选择查询来加载实体对象来更新它,而是假设DB中必须有一条记录,输入pk需要更新。