@PutMappingは、データを修正/更新するために使用されます。
package com.informanaging.project.demo.controller;
import com.informanaging.project.demo.domain.Person;
import com.informanaging.project.demo.repository.PersonRepository;
import com.informanaging.project.demo.service.PersonService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
@RequestMapping(value = "/api/person") //scopeはクラス全体なので、postPerson()は("/api/person")のURIに対応
@RestController // REST API Controllerを使用しますよとの宣言
@Slf4j
public class PersonController {
@Autowired
private PersonService personService;
@Autowired
private PersonRepository personRepository;
@GetMapping("/{id}")
public Person getPerson(@PathVariable Long id) {
return personService.getPerson(id);
}
@PostMapping
@ResponseStatus(HttpStatus.OK)
public void postPerson(@RequestBody Person person) {
personService.put(person);
log.info("person -> {}", personRepository.findAll());
}
// JSON bodyを受け取るため、@RequestBodyを使用
@PutMapping("/{id}")
public void modifyPerson(@PathVariable Long id, @RequestBody Person person){
personService.modify(id, person);
log.info("person -> {}", personRepository.findAll());
}
}
クライアントから「/api/person/1」といったRequestが送られると、@PutMapping("/{id}")のidは「1」になります。
「@RequestBody Person person」はcontent bodyであるPersonはJSONと言う宣言です。
idが「{id}」の人に対して、@RequestBodyにて受け取ったpersonのデータで更新されます。
// 省略
@Service
@Slf4j
public class PersonService {
@Autowired
private PersonRepository personRepository;
@Transactional
public void modify(Long id, Person person) {
Person personAtDb = personRepository.findById(id).orElseThrow(() -> new RuntimeException("id is not existed"));
personAtDb.setName(person.getName());
personAtDb.setPhoneNumber(person.getPhoneNumber());
personAtDb.setJob(person.getJob());
personAtDb.setAddress(person.getAddress());
personAtDb.setBirthday(new Birthday(person.getBirthday()));
personAtDb.setBloodType(person.getBloodType());
personAtDb.setHobby(person.getHobby());
personAtDb.setAge(person.getAge());
personRepository.save(personAtDb);
}
}
PersonのRepositoryから、findById(id)をして、Person Entityデータを取得します。
controller層から送られてきたpersonオブジェクトをsetterして、save()します。
// 省略
@SpringBootTest
public class PersonControllerTest {
@Autowired
private PersonController personController;
private MockMvc mockMvc;
@Test
void modifyPerson() throws Exception {
mockMvc = MockMvcBuilders.standaloneSetup(personController).build();
mockMvc.perform(
MockMvcRequestBuilders.put("/api/person/1")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content("{\n" +
" \"name\": \"martin2\", \n" +
" \"age\": 21, \n" +
" \"bloodType\": \"A\"\n" +
"}"))
.andDo(print())
.andExpect(status().isOk());
}
}
PersonControllerクラスのmodifyPerson()メソッドテストコードです。
上記のテストコードを実行すると、modifyPerson()が反応をします。
(PersonControllerのクラスに定義した@RequestMappingが「/api/person」で、modifyPersonメソッドの@PutMappingが「/id」のため)
テストコードを実行すると、idが「1」の人に対して、content bodyのデータにて更新されたことがわかります。
問題点
しかし、上記のコードでは問題が発生しています。 以下は、modifyPerson()を実行する前のGET /api/person/1 の結果です。
以下は、上記のmodifyPerson()テストコードを実行した結果です。
上記の差から分かるように、同じIDに対して、名前が違いますがそれをフィルタできなかったことと、ageとbloodTypeを変更したいですが、
変更対象以外は全部nullが入ってしまったことです。
改善
public void modifyPerson(@PathVariable Long id, @RequestBody PersonDto person)
Controllerに送られるcontentをDTO型にて受け取ります。
修正前のようにPerson entityにてマッピングしたら、entityを操作しちゃって内部データが変更されちゃう危険性があるし、
レイヤ同士でデータのやり取りはDTOが適切なわけです。
package com.informanaging.project.demo.controller.dto;
import lombok.Data;
import java.time.LocalDate;
@Data
public class PersonDto {
private String name;
private int age;
private String hobby;
private String bloodType;
private String address;
private LocalDate birthday;
private String job;
private String phoneNumber;
}
// 省略
@Service
@Slf4j
public class PersonService {
@Autowired
private PersonRepository personRepository;
@Transactional
public void modify(Long id, PersonDto personDto) {
Person person = personRepository.findById(id).orElseThrow(() -> new RuntimeException("id is not existed"));
if (!person.getName().equals(personDto.getName())) {
throw new RuntimeException("名前が違います");
}
person.set(personDto);
personRepository.save(person);
}
}
修正前のmodify()関数は、viewのrequestをentityにてマッピングしてましたが、
現在はPersonDto型にマッピングしています。
person.set(personDto); にて、personDtoのvalidatingを通したデータを
次のsave()にて保存します。
// 省略
@Entity
@NoArgsConstructor
@AllArgsConstructor
@RequiredArgsConstructor
@Data
public class Person {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // AUTO
private Long id;
@NonNull
@NotEmpty // Stringのため、NotEmptyを追加
@Column(nullable = false)
private String name;
@NonNull
@Min(1)
private int age;
private String hobby;
@NonNull
@NotEmpty // Stringのため、NotEmptyを追加
@Column(nullable = false)
private String bloodType;
private String address;
@Valid
@Embedded
private Birthday birthday;
private String job;
@ToString.Exclude
private String phoneNumber;
@OneToOne(cascade = {CascadeType.PERSIST, CascadeType.MERGE,CascadeType.REMOVE}, orphanRemoval = true, fetch = FetchType.LAZY) // persistence
@ToString.Exclude
private Block block; // personオブジェクトに対してブロックをしたか、してなかったかを確認するpropertyなのでOne-to-one
public void set(PersonDto personDto) {
// ageはintなので、ageがnullだったら、自動的に0が入ります。
// 0の場合は、設定しないとの意味。
if (personDto.getAge() != 0) {
this.setAge(personDto.getAge());
}
if(!StringUtils.isNullOrEmpty(personDto.getHobby())) {
this.setHobby(personDto.getHobby());
}
if(!StringUtils.isNullOrEmpty(personDto.getBloodType())) {
this.setBloodType(personDto.getBloodType());
}
if(!StringUtils.isNullOrEmpty(personDto.getAddress())) {
this.setAddress(personDto.getAddress());
}
if(!StringUtils.isNullOrEmpty(personDto.getPhoneNumber())) {
this.setPhoneNumber(personDto.getPhoneNumber());
}
if(!StringUtils.isNullOrEmpty(personDto.getJob())) {
this.setJob(personDto.getJob());
}
}
}
Domain層のPersonクラスにset(PersonDto personDto)関数を新しく設けて、Service層のmodify()から送られてきたPersonDtoをvalidatingできます!