簡體   English   中英

如何編寫一個RestController來從XML請求更新JPA實體,Spring Data JPA方式?

[英]How to write a RestController to update a JPA entity from an XML request, the Spring Data JPA way?

我有一個名為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注釋進行注釋,以允許將HTTP請求中的XML有效負載映射到實體的實例。

我想實現一個端點來檢索和更新具有給定id的實體。

根據對類似問題的回答 ,我需要做的就是實現如下的處理程序方法:

@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

這里提供的解決方案不起作用。

筆記2

此處提供演示此問題的完整工作代碼。

更多細節

預期行為:

  1. 加載id=1的Person實例
  2. 使用Jaxb2RootElementHttpMessageConverterMappingJackson2XmlHttpMessageConverter使用XML有效內容填充加載的人員實體的屬性
  3. 將它作為其person參數傳遞給控制器​​的動作處理程序

實際行為:

  1. 加載了id=1的Person實例
  2. 實例的屬性未更新以匹配請求有效內容中的XML
  3. 傳遞給控制器​​的操作處理程序方法的person實例的屬性不會更新

這個'@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. 控制器中的錯誤映射
  2. 更新你需要得到它在持續(管理)國家第一個實體,然后復制所需就可以了狀態。
  3. 考慮為您的業務對象引入DTO,因為稍后,使用持久化狀態實體進行響應可能會導致麻煩(例如,不期望的延遲集合提取或實體關系序列化為XML,JSON可能由於無限方法調用而導致堆棧溢出)

以下是修復測試的簡單案例:

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

問題是,當您調用personRepository.save(person)您的person實體沒有主鍵字段(id),因此數據庫最終會有兩條記錄,其中新記錄主鍵由db生成。 修復將是為您的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)的更新操作並調用它。

要更新任何實體,加載和保存必須在同一個Transaction中,否則它將在save()調用時創建新的實體,或者將拋出重復的主鍵約束違例異常。

要更新我們需要將實體,load()/ find()和save()放在同一個事務中,或者在@Repository類中編寫JPQL UPDATE查詢,並使用@Modifying注釋該方法。

@Modifying注釋不會觸發額外的選擇查詢來加載實體對象來更新它,而是假設DB中必須有一條帶有輸入pk的記錄,需要更新。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM