diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..0de3cb43a --- /dev/null +++ b/.gitignore @@ -0,0 +1,49 @@ +HELP.md + +### Gradle ### +gradle/ +gradlew +gradlew.bat +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store + +### Log files ### +logs/ +*.log diff --git a/build.gradle b/build.gradle new file mode 100644 index 000000000..f5efa886b --- /dev/null +++ b/build.gradle @@ -0,0 +1,38 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '2.7.14' + id 'io.spring.dependency-management' version '1.0.15.RELEASE' +} + +group = 'com.programmers' +version = '0.0.1-SNAPSHOT' + +java { + sourceCompatibility = '17' +} + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-web' + + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + + runtimeOnly 'com.h2database:h2' + + testImplementation 'org.springframework.boot:spring-boot-starter-test' +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 000000000..c58e92312 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'jpa-basic' diff --git a/src/main/java/com/programmers/jpabasic/JpaBasicApplication.java b/src/main/java/com/programmers/jpabasic/JpaBasicApplication.java new file mode 100644 index 000000000..e574da9be --- /dev/null +++ b/src/main/java/com/programmers/jpabasic/JpaBasicApplication.java @@ -0,0 +1,14 @@ +package com.programmers.jpabasic; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +@EnableJpaAuditing +@SpringBootApplication +public class JpaBasicApplication { + + public static void main(String[] args) { + SpringApplication.run(JpaBasicApplication.class, args); + } +} diff --git a/src/main/java/com/programmers/jpabasic/domain/customer/entity/Customer.java b/src/main/java/com/programmers/jpabasic/domain/customer/entity/Customer.java new file mode 100644 index 000000000..8f7d8527a --- /dev/null +++ b/src/main/java/com/programmers/jpabasic/domain/customer/entity/Customer.java @@ -0,0 +1,33 @@ +package com.programmers.jpabasic.domain.customer.entity; + +import javax.persistence.Embedded; +import javax.persistence.Entity; + +import com.programmers.jpabasic.global.common.BaseEntity; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Builder +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Customer extends BaseEntity { + + @Embedded + private Name name; + + public static Customer create(String firstName, String lastName) { + return Customer.builder() + .name(Name.of(firstName, lastName)) + .build(); + } + + public void updateName(String firstName, String lastName) { + this.name = Name.of(firstName, lastName); + } +} diff --git a/src/main/java/com/programmers/jpabasic/domain/customer/entity/Name.java b/src/main/java/com/programmers/jpabasic/domain/customer/entity/Name.java new file mode 100644 index 000000000..525420581 --- /dev/null +++ b/src/main/java/com/programmers/jpabasic/domain/customer/entity/Name.java @@ -0,0 +1,31 @@ +package com.programmers.jpabasic.domain.customer.entity; + +import javax.persistence.Column; +import javax.persistence.Embeddable; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@Embeddable +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Name { + + @Column(nullable = false, length = 30) + private String firstName; + + @Column(nullable = false, length = 20) + private String lastName; + + public static Name of(String firstName, String lastName) { + return Name.builder() + .firstName(firstName) + .lastName(lastName) + .build(); + } +} diff --git a/src/main/java/com/programmers/jpabasic/domain/customer/repository/CustomerRepository.java b/src/main/java/com/programmers/jpabasic/domain/customer/repository/CustomerRepository.java new file mode 100644 index 000000000..03bd882b1 --- /dev/null +++ b/src/main/java/com/programmers/jpabasic/domain/customer/repository/CustomerRepository.java @@ -0,0 +1,8 @@ +package com.programmers.jpabasic.domain.customer.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.programmers.jpabasic.domain.customer.entity.Customer; + +public interface CustomerRepository extends JpaRepository { +} diff --git a/src/main/java/com/programmers/jpabasic/domain/member/entity/Member.java b/src/main/java/com/programmers/jpabasic/domain/member/entity/Member.java new file mode 100644 index 000000000..027d3943c --- /dev/null +++ b/src/main/java/com/programmers/jpabasic/domain/member/entity/Member.java @@ -0,0 +1,35 @@ +package com.programmers.jpabasic.domain.member.entity; + +import javax.persistence.Column; +import javax.persistence.Entity; + +import com.programmers.jpabasic.global.common.BaseEntity; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Builder +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Member extends BaseEntity { + + @Column(nullable = false, length = 50) + private String name; + + @Column(nullable = false, length = 30, unique = true) + private String nickname; + + @Column + private int age; + + @Column(nullable = false) + private String address; + + @Column(length = 100) + private String description; +} diff --git a/src/main/java/com/programmers/jpabasic/domain/order/entity/Item.java b/src/main/java/com/programmers/jpabasic/domain/order/entity/Item.java new file mode 100644 index 000000000..2fcfb83fc --- /dev/null +++ b/src/main/java/com/programmers/jpabasic/domain/order/entity/Item.java @@ -0,0 +1,26 @@ +package com.programmers.jpabasic.domain.order.entity; + +import javax.persistence.Column; +import javax.persistence.Entity; + +import com.programmers.jpabasic.global.common.BaseEntity; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Builder +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Item extends BaseEntity { + + @Column(nullable = false) + private int price; + + @Column + private int stockQuantity; +} diff --git a/src/main/java/com/programmers/jpabasic/domain/order/entity/Order.java b/src/main/java/com/programmers/jpabasic/domain/order/entity/Order.java new file mode 100644 index 000000000..491a65ce7 --- /dev/null +++ b/src/main/java/com/programmers/jpabasic/domain/order/entity/Order.java @@ -0,0 +1,47 @@ +package com.programmers.jpabasic.domain.order.entity; + +import java.util.ArrayList; +import java.util.List; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.FetchType; +import javax.persistence.JoinColumn; +import javax.persistence.Lob; +import javax.persistence.ManyToOne; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +import com.programmers.jpabasic.domain.member.entity.Member; +import com.programmers.jpabasic.global.common.BaseEntity; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Builder +@Table(name = "orders") +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Order extends BaseEntity { + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private Member member; + + @Column(nullable = false) + @Enumerated(value = EnumType.STRING) + private OrderStatus status; + + @Lob + private String memo; + + @OneToMany(mappedBy = "order") + private final List orderItems = new ArrayList<>(); +} diff --git a/src/main/java/com/programmers/jpabasic/domain/order/entity/OrderItem.java b/src/main/java/com/programmers/jpabasic/domain/order/entity/OrderItem.java new file mode 100644 index 000000000..b0537d1fe --- /dev/null +++ b/src/main/java/com/programmers/jpabasic/domain/order/entity/OrderItem.java @@ -0,0 +1,37 @@ +package com.programmers.jpabasic.domain.order.entity; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; + +import com.programmers.jpabasic.global.common.BaseEntity; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Builder +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class OrderItem extends BaseEntity { + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "order_id") + private Order order; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "item_id") + private Item item; + + @Column(nullable = false) + private int price; + + @Column(nullable = false) + private int quantity; +} diff --git a/src/main/java/com/programmers/jpabasic/domain/order/entity/OrderStatus.java b/src/main/java/com/programmers/jpabasic/domain/order/entity/OrderStatus.java new file mode 100644 index 000000000..a6342b979 --- /dev/null +++ b/src/main/java/com/programmers/jpabasic/domain/order/entity/OrderStatus.java @@ -0,0 +1,5 @@ +package com.programmers.jpabasic.domain.order.entity; + +public enum OrderStatus { + OPENED, CANCELLED +} diff --git a/src/main/java/com/programmers/jpabasic/global/common/BaseEntity.java b/src/main/java/com/programmers/jpabasic/global/common/BaseEntity.java new file mode 100644 index 000000000..c95a683cf --- /dev/null +++ b/src/main/java/com/programmers/jpabasic/global/common/BaseEntity.java @@ -0,0 +1,34 @@ +package com.programmers.jpabasic.global.common; + +import java.time.LocalDateTime; + +import javax.persistence.Column; +import javax.persistence.EntityListeners; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.MappedSuperclass; + +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import lombok.Getter; + +@Getter +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public abstract class BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) + private Long id; + + @CreatedDate + @Column(nullable = false, updatable = false, columnDefinition = "TIMESTAMP") + private LocalDateTime createdAt; + + @LastModifiedDate + @Column(nullable = false, columnDefinition = "TIMESTAMP") + private LocalDateTime updatedAt; +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 000000000..c2ab783bc --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,5 @@ +spring: + jpa: + properties: + hibernate: + format_sql: true diff --git a/src/test/java/com/programmers/jpabasic/domain/customer/entity/CustomerPersistenceTest.java b/src/test/java/com/programmers/jpabasic/domain/customer/entity/CustomerPersistenceTest.java new file mode 100644 index 000000000..b6712282e --- /dev/null +++ b/src/test/java/com/programmers/jpabasic/domain/customer/entity/CustomerPersistenceTest.java @@ -0,0 +1,122 @@ +package com.programmers.jpabasic.domain.customer.entity; + +import static org.assertj.core.api.Assertions.*; + +import javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; +import javax.persistence.EntityTransaction; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; + +import com.programmers.jpabasic.support.CustomerFixture; + +@DataJpaTest +class CustomerPersistenceTest { + + @Autowired + private EntityManagerFactory entityManagerFactory; + + private EntityManager entityManager; + private EntityTransaction transaction; + + @BeforeEach + void setUp() { + entityManager = entityManagerFactory.createEntityManager(); + transaction = entityManager.getTransaction(); + transaction.begin(); + } + + @Test + @DisplayName("고객 저장에 성공한다.") + void save() { + // given + Customer customer = CustomerFixture.create("heebin", "kim"); + + // when + entityManager.persist(customer); + transaction.commit(); + + // then + Customer result = entityManager.find(Customer.class, customer.getId()); + assertThat(result.getName().getFirstName()).isEqualTo("heebin"); + assertThat(result.getName().getLastName()).isEqualTo("kim"); + } + + @Test + @DisplayName("DB로부터 고객 조회에 성공한다.") + void findFromDB() { + // given + Customer customer = CustomerFixture.create("heebin", "kim"); + + entityManager.persist(customer); + transaction.commit(); + + // when + entityManager.detach(customer); + Customer result = entityManager.find(Customer.class, customer.getId()); + + // then + assertThat(result.getName().getFirstName()).isEqualTo("heebin"); + assertThat(result.getName().getLastName()).isEqualTo("kim"); + } + + @Test + @DisplayName("1차 캐시로부터 고객 조회에 성공한다.") + void findFromCache() { + // given + Customer customer = CustomerFixture.create("heebin", "kim"); + + entityManager.persist(customer); + transaction.commit(); + + // when + Customer result = entityManager.find(Customer.class, customer.getId()); + + // then + assertThat(result.getName().getFirstName()).isEqualTo("heebin"); + assertThat(result.getName().getLastName()).isEqualTo("kim"); + } + + @Test + @DisplayName("고객 수정에 성공한다.") + void update() { + // given + Customer customer = CustomerFixture.create("heebin", "kim"); + + entityManager.persist(customer); + transaction.commit(); + + // when + transaction.begin(); + customer.updateName("희빈", "김"); + transaction.commit(); + + // then + Customer result = entityManager.find(Customer.class, customer.getId()); + assertThat(result.getName().getFirstName()).isEqualTo("희빈"); + assertThat(result.getName().getLastName()).isEqualTo("김"); + } + + @Test + @DisplayName("고객 삭제에 성공한다.") + void delete() { + // given + Customer customer = CustomerFixture.create("heebin", "kim"); + + entityManager.persist(customer); + transaction.commit(); + + // when + transaction.begin(); + entityManager.remove(customer); + transaction.commit(); + + // then + Customer result = entityManager.find(Customer.class, customer.getId()); + assertThat(result).isNull(); + } +} diff --git a/src/test/java/com/programmers/jpabasic/domain/customer/repository/CustomerRepositoryTest.java b/src/test/java/com/programmers/jpabasic/domain/customer/repository/CustomerRepositoryTest.java new file mode 100644 index 000000000..4e4ad1ca9 --- /dev/null +++ b/src/test/java/com/programmers/jpabasic/domain/customer/repository/CustomerRepositoryTest.java @@ -0,0 +1,97 @@ +package com.programmers.jpabasic.domain.customer.repository; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; + +import com.programmers.jpabasic.domain.customer.entity.Customer; +import com.programmers.jpabasic.support.CustomerFixture; + +@DataJpaTest +class CustomerRepositoryTest { + + @Autowired + private CustomerRepository customerRepository; + + @Test + @DisplayName("고객 저장에 성공한다.") + void save() { + // given + Customer customer = CustomerFixture.create("heebin", "kim"); + + // when + customerRepository.save(customer); + + // then + Customer result = customerRepository.findById(customer.getId()).orElseThrow(); + assertThat(result.getName().getFirstName()).isEqualTo("heebin"); + assertThat(result.getName().getLastName()).isEqualTo("kim"); + } + + @Test + @DisplayName("모든 고객 조회에 성공한다.") + void findAll() { + // given + Customer customer1 = CustomerFixture.create("heebin", "kim"); + Customer customer2 = CustomerFixture.create("희빈", "김"); + + customerRepository.save(customer1); + customerRepository.save(customer2); + + // when + List result = customerRepository.findAll(); + + // then + assertThat(result).hasSize(2).contains(customer1, customer2); + } + + @Test + @DisplayName("고객 ID로 조회에 성공한다.") + void findById() { + // given + Customer customer = CustomerFixture.create("heebin", "kim"); + customerRepository.save(customer); + + // when + Customer result = customerRepository.findById(customer.getId()).orElseThrow(); + + // then + assertThat(result.getName().getFirstName()).isEqualTo("heebin"); + assertThat(result.getName().getLastName()).isEqualTo("kim"); + } + + @Test + @DisplayName("고객 수정에 성공한다.") + void update() { + // given + Customer customer = CustomerFixture.create("heebin", "kim"); + customerRepository.save(customer); + + // when + customer.updateName("희빈", "김"); + + // then + Customer result = customerRepository.findById(customer.getId()).orElseThrow(); + assertThat(result.getName().getFirstName()).isEqualTo("희빈"); + assertThat(result.getName().getLastName()).isEqualTo("김"); + } + + @Test + @DisplayName("고객 삭제에 성공한다.") + void delete() { + // given + Customer customer = CustomerFixture.create("heebin", "kim"); + customerRepository.save(customer); + + // when + customerRepository.delete(customer); + + // then + assertThat(customerRepository.findById(customer.getId())).isEmpty(); + } +} diff --git a/src/test/java/com/programmers/jpabasic/support/CustomerFixture.java b/src/test/java/com/programmers/jpabasic/support/CustomerFixture.java new file mode 100644 index 000000000..eede3104b --- /dev/null +++ b/src/test/java/com/programmers/jpabasic/support/CustomerFixture.java @@ -0,0 +1,13 @@ +package com.programmers.jpabasic.support; + +import com.programmers.jpabasic.domain.customer.entity.Customer; +import com.programmers.jpabasic.domain.customer.entity.Name; + +public class CustomerFixture { + public static Customer create(String firstName, String lastName) { + return Customer + .builder() + .name(Name.of(firstName, lastName)) + .build(); + } +}