【问题标题】:How to write a RestController to update a JPA entity from an XML request, the Spring Data JPA way?如何编写 RestController 以从 XML 请求更新 JPA 实体,Spring Data JPA 方式?
【发布时间】:2017-05-10 05:11:33
【问题描述】:

我有一个数据库,其中有一个名为 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, 我需要做的就是实现 handler 方法如下:

@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 加载Person 实例
  2. 使用Jaxb2RootElementHttpMessageConverterMappingJackson2XmlHttpMessageConverter 使用XML 有效负载填充已加载人员实体的属性
  3. 将其作为 person 参数传递给控制器​​的操作处理程序

实际行为:

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

【问题讨论】:

  • 它是如何失败的?响应实际上包含什么?它是否将人保存在数据库中?当您使用调试器并查看会发生什么时会发生什么?您需要阅读错误消息并进行调查。
  • 有人投票关闭它,因为它没有编程!开什么玩笑。
  • 别人问了你一些问题。
  • 我已经回答了他(在问题的最底部)。我还提供了完整的源代码以及演示该问题的集成测试。
  • 我在更多详情部分添加了更多信息。如果您需要更多详细信息,请告诉我。

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


【解决方案1】:

这个 '@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);
   }

【讨论】:

  • 无论您在测试中进行什么更改(甚至可以为空),spring 都会从数据库中加载人员(实际上忽略了您的输入)。对于 URL 编码表单数据,加载人员并根据请求数据更新其属性。那么问题就变成了,如何优雅地“合并”呢?假设您的实体有十几个属性和关联。手动将现有实体与请求正文合并变得非常混乱。如果你有不止一个实体,那就更糟了。据我所知,Spring 缺少这个非常重要的特性。
  • 是的,但是 spring 不知道您是否要保留来自 db 的数据,或者它应该只是挑选其中一些并更新。您可以从输入中获取整个“人”并保存它(您有一个 id 集),或者决定您需要使用转换器更新哪个字段。另一件事:我不建议将域对象直接发送到控制器 - 正如你所说 - 它可能是复杂的对象。定义您真正可以通过 API 更改的内容,创建从模型到域的模型对象 + 转换器。你可以看看推土机 - dozer.sourceforge.net/documentation/about.html
  • @Behrang 所以问题就变成了,如何优雅地“合并”?试试BeanUtils.copyProperties(found, person);commons.apache.org/proper/commons-beanutils/apidocs/org/apache/…
  • +1,在真正发现这个是一样的之前发布了我的答案。 DTO 和 Dozer 也是 +1 - 返回托管状态对象会带来很多麻烦。
【解决方案2】:
  1. 控制器映射错误
  2. 更新实体,您需要先使其处于持久(托管)状态,然后在其上复制所需的状态。
  3. 考虑为您的业务对象引入 DTO,因为稍后使用持久状态实体进行响应可能会导致问题(例如,不希望的延迟集合获取或实体关系序列化为 XML,JSON 可能会由于无限方法调用而导致 stackoverflow)

以下是修复测试的简单案例:

@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;
}

【讨论】:

  • 对于 1. 如果您阅读文档,您会看到 Spring Data JPA 与 MVC 集成以像这样工作。实际上 1. 是起作用的部分。
  • 很好,我不知道。无论如何,您是否尝试过调试您的测试?使用@ModelAttribute,您将获得持久的 Person 实体注入。我还没有看到您在哪里拥有具有新 Person 状态的请求正文以及您在哪里合并这些实体。请参阅我的更新答案。
【解决方案3】:

问题是当您调用personRepository.save(person) 时,您的个人实体没有主键字段(id),因此数据库最终有两条记录,其中新记录的主键由数据库生成。解决方法是为您的 id 字段创建一个设置器,并在保存之前使用它来设置实体的 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 并将其转换为域模型。此外,如果您不想为主键字段创建设置器,另一种选择可能是支持基于任何其他唯一字段(如 externalId)的更新操作并改为调用它。

【讨论】:

  • 我认为两条记录的原因是 src/main/resources 下的 import.sql 有插入 sql。如果您删除该脚本,则只会生成 id 为 1 的一行。如果您查看断言 Expected: "Tin Tin" but: was "Tin",您会看到“Tin”来自插入脚本。
  • 是的,但他希望更新该记录,因此“PUT”调用对吗?
  • 是的,他正在“PUT”更新已创建的记录,但由于他没有映射 id,因此创建了一个新条目。说得通。谢谢。
  • _问题是当您调用 personRepository.save(person) 时,您的 person 实体没有主键字段(id)_。它有。我已经上传了示例代码(见问题),表明这不是问题。
  • 我已经尝试过您的示例@Behrang,它确实具有主键字段,但访问器(setter 和 getter)不公开,并且您在调用 personRepository.save 时没有填充它。在您调用personRepository.save 之前,您会看到在您的版本id 中没有填充Person。如果正确填充,您将不会看到此问题。
【解决方案4】:

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

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-08-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-02-06
    • 2019-06-15
    • 1970-01-01
    • 2015-09-11
    相关资源
    最近更新 更多