diff --git a/build.gradle b/build.gradle index 9e9e67ddf..fdf0daf5a 100644 --- a/build.gradle +++ b/build.gradle @@ -24,6 +24,7 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-validation' compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.h2database:h2' annotationProcessor 'org.projectlombok:lombok' diff --git a/src/main/java/com/programmers/springbootjpa/controller/CustomerController.java b/src/main/java/com/programmers/springbootjpa/controller/CustomerController.java new file mode 100644 index 000000000..c25708b19 --- /dev/null +++ b/src/main/java/com/programmers/springbootjpa/controller/CustomerController.java @@ -0,0 +1,63 @@ +package com.programmers.springbootjpa.controller; + +import com.programmers.springbootjpa.dto.request.CustomerCreateRequest; +import com.programmers.springbootjpa.dto.request.CustomerUpdateRequest; +import com.programmers.springbootjpa.dto.response.CustomerResponse; +import com.programmers.springbootjpa.service.CustomerService; +import jakarta.validation.Valid; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + + +@RequestMapping("/api/customers") +@RequiredArgsConstructor +@RestController +public class CustomerController { + + private final CustomerService customerService; + + @PostMapping + public ResponseEntity createCustomer(@Valid @RequestBody CustomerCreateRequest customerCreateRequest) { + customerService.createCustomer(customerCreateRequest); + + return ResponseEntity.status(HttpStatus.CREATED).body(null); + } + + @GetMapping("/{id}") + public ResponseEntity readCustomer(@PathVariable Long id) { + CustomerResponse customerResponse = customerService.readCustomer(id); + + return ResponseEntity.ok(customerResponse); + } + + @GetMapping + public ResponseEntity> readAllCustomer() { + List customerResponses = customerService.readAllCustomer(); + + return ResponseEntity.ok(customerResponses); + } + + @PatchMapping + public ResponseEntity updateCustomer(@Valid @RequestBody CustomerUpdateRequest customerUpdateRequest) { + customerService.updateCustomer(customerUpdateRequest); + + return ResponseEntity.ok(null); + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteCustomer(@PathVariable Long id) { + customerService.deleteCustomer(id); + + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/com/programmers/springbootjpa/domain/Address.java b/src/main/java/com/programmers/springbootjpa/domain/Address.java new file mode 100644 index 000000000..47b087c2b --- /dev/null +++ b/src/main/java/com/programmers/springbootjpa/domain/Address.java @@ -0,0 +1,26 @@ +package com.programmers.springbootjpa.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Embeddable +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Address { + + @Column(name = "street_address", nullable = false) + private String streetAddress; + @Column(name = "detailed_address", nullable = false) + private String detailedAddress; + @Column(name = "zip_code", nullable = false) + private Integer zipCode; + + public Address(String streetAddress, String detailedAddress, Integer zipCode) { + this.streetAddress = streetAddress; + this.detailedAddress = detailedAddress; + this.zipCode = zipCode; + } +} diff --git a/src/main/java/com/programmers/springbootjpa/domain/Customer.java b/src/main/java/com/programmers/springbootjpa/domain/Customer.java new file mode 100644 index 000000000..9d2e9eaac --- /dev/null +++ b/src/main/java/com/programmers/springbootjpa/domain/Customer.java @@ -0,0 +1,50 @@ +package com.programmers.springbootjpa.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Customer { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) + private Long id; + + @Column(name = "name", nullable = false, length = 20) + private String name; + + @Column(name = "age", nullable = false) + private Integer age; + + @Column(name = "nick_name", nullable = false) + private String nickName; + + @Embedded + private Address address; + + @Builder + public Customer(String name, Integer age, String nickName, Address address) { + this.name = name; + this.age = age; + this.nickName = nickName; + this.address = address; + } + + public void changeNickName(String nickName) { + this.nickName = nickName; + } + + public void changeAddress(Address address) { + this.address = address; + } +} diff --git a/src/main/java/com/programmers/springbootjpa/dto/request/CustomerCreateRequest.java b/src/main/java/com/programmers/springbootjpa/dto/request/CustomerCreateRequest.java new file mode 100644 index 000000000..3616e09b0 --- /dev/null +++ b/src/main/java/com/programmers/springbootjpa/dto/request/CustomerCreateRequest.java @@ -0,0 +1,37 @@ +package com.programmers.springbootjpa.dto.request; + +import com.programmers.springbootjpa.domain.Address; +import com.programmers.springbootjpa.domain.Customer; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class CustomerCreateRequest { + + @NotBlank(message = "이름은 공백이거나 값이 없으면 안됩니다.") + private String name; + @Min(value = 1, message = "나이는 한살보다 많아야합니다.") + @Max(value = 100, message = "나이는 백살보다 적여야합니다.") + @NotNull(message = "나이는 값이 없으면 안됩니다.") + private Integer age; + @NotBlank(message = "닉네임은 공백이거나 값이 없으면 안됩니다.") + private String nickName; + @NotNull(message = "주소값은 없을 수 없습니다.") + private Address address; + + public Customer of() { + return Customer.builder() + .name(name) + .age(age) + .nickName(nickName) + .address(address) + .build(); + } + +} diff --git a/src/main/java/com/programmers/springbootjpa/dto/request/CustomerUpdateRequest.java b/src/main/java/com/programmers/springbootjpa/dto/request/CustomerUpdateRequest.java new file mode 100644 index 000000000..db878de91 --- /dev/null +++ b/src/main/java/com/programmers/springbootjpa/dto/request/CustomerUpdateRequest.java @@ -0,0 +1,22 @@ +package com.programmers.springbootjpa.dto.request; + +import com.programmers.springbootjpa.domain.Address; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.PositiveOrZero; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class CustomerUpdateRequest { + + @PositiveOrZero(message = "id값은 0이상 이어야합니다.") + private Long id; + @NotBlank(message = "닉네임은 공백이거나 값이 없으면 안됩니다.") + private String nickName; + @NotNull(message = "주소값은 없을 수 없습니다.") + private Address address; + +} diff --git a/src/main/java/com/programmers/springbootjpa/dto/response/CustomerResponse.java b/src/main/java/com/programmers/springbootjpa/dto/response/CustomerResponse.java new file mode 100644 index 000000000..53a2914e5 --- /dev/null +++ b/src/main/java/com/programmers/springbootjpa/dto/response/CustomerResponse.java @@ -0,0 +1,23 @@ +package com.programmers.springbootjpa.dto.response; + +import com.programmers.springbootjpa.domain.Address; +import com.programmers.springbootjpa.domain.Customer; +import lombok.Getter; + +@Getter +public class CustomerResponse { + + private final Long id; + private final String name; + private final Integer age; + private final String nickName; + private final Address address; + + public CustomerResponse(Customer customer) { + this.id = customer.getId(); + this.name = customer.getName(); + this.age = customer.getAge(); + this.nickName = customer.getNickName(); + this.address = customer.getAddress(); + } +} diff --git a/src/main/java/com/programmers/springbootjpa/repository/CustomerRepository.java b/src/main/java/com/programmers/springbootjpa/repository/CustomerRepository.java new file mode 100644 index 000000000..bf639bb3a --- /dev/null +++ b/src/main/java/com/programmers/springbootjpa/repository/CustomerRepository.java @@ -0,0 +1,8 @@ +package com.programmers.springbootjpa.repository; + +import com.programmers.springbootjpa.domain.Customer; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface CustomerRepository extends JpaRepository { + +} diff --git a/src/main/java/com/programmers/springbootjpa/service/CustomerService.java b/src/main/java/com/programmers/springbootjpa/service/CustomerService.java new file mode 100644 index 000000000..954c13447 --- /dev/null +++ b/src/main/java/com/programmers/springbootjpa/service/CustomerService.java @@ -0,0 +1,64 @@ +package com.programmers.springbootjpa.service; + +import com.programmers.springbootjpa.domain.Customer; +import com.programmers.springbootjpa.dto.request.CustomerCreateRequest; +import com.programmers.springbootjpa.dto.request.CustomerUpdateRequest; +import com.programmers.springbootjpa.dto.response.CustomerResponse; +import com.programmers.springbootjpa.repository.CustomerRepository; +import java.util.List; +import java.util.NoSuchElementException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class CustomerService { + + private final CustomerRepository customerRepository; + + @Transactional + public void createCustomer(CustomerCreateRequest customerCreateRequest) { + Customer customer = Customer.builder() + .name(customerCreateRequest.getName()) + .age(customerCreateRequest.getAge()) + .nickName(customerCreateRequest.getNickName()) + .address(customerCreateRequest.getAddress()) + .build(); + + customerRepository.save(customer); + } + + public List readAllCustomer() { + List customers = customerRepository.findAll(); + + return customers.stream() + .map(CustomerResponse::new) + .toList(); + } + + public CustomerResponse readCustomer(Long id) { + Customer customer = customerRepository.findById(id) + .orElseThrow(() -> new NoSuchElementException("찾는 사용자가 없습니다.")); + + return new CustomerResponse(customer); + } + + @Transactional + public void updateCustomer(CustomerUpdateRequest customerUpdateRequest) { + Customer customer = customerRepository.findById(customerUpdateRequest.getId()) + .orElseThrow(() -> new NoSuchElementException("업데이트 할 사용자가 없습니다.")); + + customer.changeNickName(customerUpdateRequest.getNickName()); + customer.changeAddress(customerUpdateRequest.getAddress()); + } + + @Transactional + public void deleteCustomer(Long id) { + customerRepository.findById(id) + .orElseThrow(() -> new NoSuchElementException("삭제할 사용자가 없습니다.")); + + customerRepository.deleteById(id); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index 8b1378917..000000000 --- a/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 000000000..1f6280ea6 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,21 @@ +spring: + datasource: + driver-class-name: org.h2.Driver + username: sa + password: + jpa: + hibernate: + ddl-auto: create-drop + database-platform: org.hibernate.dialect.H2Dialect + properties: + hibernate: + format_sql: true + +logging: + level: + org: + hibernate: + SQL: debug + type: + descriptor: + sql: trace diff --git a/src/test/java/com/programmers/springbootjpa/PersistenceContextTest.java b/src/test/java/com/programmers/springbootjpa/PersistenceContextTest.java new file mode 100644 index 000000000..a0de5a140 --- /dev/null +++ b/src/test/java/com/programmers/springbootjpa/PersistenceContextTest.java @@ -0,0 +1,124 @@ +package com.programmers.springbootjpa; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.programmers.springbootjpa.domain.Address; +import com.programmers.springbootjpa.domain.Customer; +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.EntityTransaction; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +public class PersistenceContextTest { + + @Autowired + EntityManagerFactory entityManagerFactory; + + @Test + @DisplayName("고객을 영속 상태로 만들 수 있다.") + void persistCustomer() { + // given + EntityManager entityManager = entityManagerFactory.createEntityManager(); + EntityTransaction entityTransaction = entityManager.getTransaction(); + + entityTransaction.begin(); + Customer customer = Customer.builder() + .name("이현호") + .nickName("황창현") + .age(27) + .address(new Address("서울특별시 영등포구 도신로", "XX빌딩", 10000)) + .build(); + + // when + entityManager.persist(customer); + entityTransaction.commit(); + + Customer persistedCustomer = entityManager.find(Customer.class, 1L); + + // then + assertThat(persistedCustomer).usingRecursiveComparison().isEqualTo(customer); + } + + @Test + @DisplayName("고객을 준영속 상태로 만들 수 있다.") + void semiPersistCustomer() { + // given + EntityManager entityManager = entityManagerFactory.createEntityManager(); + EntityTransaction entityTransaction = entityManager.getTransaction(); + + entityTransaction.begin(); + Customer customer = Customer.builder() + .name("이현호") + .nickName("황창현") + .age(27) + .address(new Address("서울특별시 영등포구 도신로", "XX빌딩", 10000)) + .build(); + + // when + entityManager.persist(customer); + entityTransaction.commit(); + + entityManager.detach(customer); + boolean isSemiPersisted = entityManager.contains(customer); + + // then + assertThat(isSemiPersisted).isFalse(); + } + + @Test + @DisplayName("고객을 비영속 상태로 만들 수 있다.") + void nonPersistCustomer() { + // given + EntityManager entityManager = entityManagerFactory.createEntityManager(); + EntityTransaction entityTransaction = entityManager.getTransaction(); + + entityTransaction.begin(); + Customer customer = Customer.builder() + .name("이현호") + .nickName("황창현") + .age(27) + .address(new Address("서울특별시 영등포구 도신로", "XX빌딩", 10000)) + .build(); + + // when + entityTransaction.commit(); + + boolean isNonPersisted = entityManager.contains(customer); + + // then + assertThat(isNonPersisted).isFalse(); + } + + @Test + @DisplayName("고객을 영속성 컨텍스트에서 삭제할 수 있다.") + void removeCustomerFromPersistenceContext() { + // given + EntityManager entityManager = entityManagerFactory.createEntityManager(); + EntityTransaction entityTransaction = entityManager.getTransaction(); + + entityTransaction.begin(); + Customer customer = Customer.builder() + .name("이현호") + .nickName("황창현") + .age(27) + .address(new Address("서울특별시 영등포구 도신로", "XX빌딩", 10000)) + .build(); + + // when + entityManager.persist(customer); + entityTransaction.commit(); + entityManager.remove(customer); + + boolean isRemovedFromPersistenceContext = entityManager.contains(customer); + Customer actualCustomer = entityManager.find(Customer.class, 1L); + + // then + assertThat(isRemovedFromPersistenceContext).isFalse(); + assertThat(actualCustomer).isNull(); + } + +} diff --git a/src/test/java/com/programmers/springbootjpa/SpringbootJpaApplicationTests.java b/src/test/java/com/programmers/springbootjpa/SpringbootJpaApplicationTests.java deleted file mode 100644 index b530064fb..000000000 --- a/src/test/java/com/programmers/springbootjpa/SpringbootJpaApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.programmers.springbootjpa; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class SpringbootJpaApplicationTests { - - @Test - void contextLoads() { - } - -}