Spring Data JPA
by JiwonDev๊ธฐ์ต์๋ ๋ ์ฝ๊ฒ ์ฐพ์ผ๋ ค๊ณ ํ ๊ธ์ ๋ค ์ ์์ต๋๋ค.
* ์คํฌ๋กค ์๋ฐ ์ฃผ์ *
์คํ๋ง ์ค์ ์ ์ฝ๊ฒ ํด์ฃผ๋ Spring Boot ํ๋ก์ ํธ๊ฐ ์๋ฏ์ด
์คํ๋ง์์ ๋ฐ์ดํฐ๋ฅผ ์ฝ๊ฒ ์ฌ์ฉํ๊ฒ ํด์ฃผ๋ Spring Data ํ๋ก์ ํธ๊ฐ ์๋ค.
๊ทธ์ค ์คํ๋ง์์ JPA๋ฅผ ์ถ์ํํ์ฌ, ์ ๋ง ์ฝ๊ฒ ์ฌ์ฉํ ์ ์๊ฒ ๋ง๋ Spring Data JPA๋ฅผ ์์๋ณด์.
๐ญ ์ด๋ป๊ฒ ์ฌ์ฉํ๋์
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
์ฐธ๊ณ ๋ก spring-boot-data-jpa์๋ ์๋์ ํจํค์ง๋ค์ ํฌํจํ๊ณ ์๋ค.
ํจํค์ง ๋ด๋ถ์์ Spring Core๋ฅผ ์ฌ์ฉํ๊ณ ์์ผ๋ฏ๋ก, gradle์ด spring core ์์กด์ฑ๊น์ง ๋ถ๋ฌ์จ๋ค.
- Jakarta EE(๊ตฌ Java EE)์ persistence, transaction ๋ชจ๋
- hibernate-core (JPA ๊ตฌํ์ฒด, ํ์ด๋ฒ๋ค์ดํธ)
- spring data-jpa
- spring aspectJ
- spring-boot-starter aop
- spring-boot-starter jdbc
์์ฆ์ ์คํ๋ง๋ถํธ๋ฅผ ์ฌ์ฉํ๋ฏ๋ก, @SpringBootApplication ๋ฅผ ์ฌ์ฉํ๋ค๋ฉด ํน๋ณํ ๋ฐ๋ก ํด์ค ์ค์ ์ ์๋ค.
๋ฌผ๋ก public class Application ์ด ์ต์์ ํจํค์ง๊ฐ ์๋๋ผ๋ฉด ์ฐพ์ง ๋ชปํ๋ค. ์ด ๊ฒฝ์ฐ์๋ง ์ง์ ์ง์ ํด์ฃผ์.
package mypackage.myapp; // ์ด ๋ชจ๋์ ํ์ ํจํค์ง๋ฅผ ๋ชจ๋ ์ ์ฉ์ํจ๋ค.
@SpringBootApplication
public class DataJpaApplication {
public static void main(String[] args) {
SpringApplication.run(DataJpaApplication.class, args);
}
}
๋ง์ฝ ์คํ๋ง ๋ถํธ๋ฅผ ์ฌ์ฉํ์ง ์๋๋ค๋ฉด์?
@EnableJpaRepositories( ์ค์บ ๊ฒฝ๋ก )๋ฅผ ์ง์ ํด์ฃผ๋ฉด ๋๋ค. ๋ณดํต ํ๋ก์ ํธ ์ต์๋จ์ ์๋ ์ค์ ๊ฐ์ฒด์ @Configuration ์ ํจ๊ป ์์ฑํ๋ค.
@Configuration
@EnableJpaRepositories(basePackages = "jpabook.jpashop.repository")
public class AppConfig {}
@Entity์ ๊ฒฝ์ฐ ์คํ๋ง๊ณผ ์ฐ๊ด์์ด ๊ทธ๋ฅ JPA ๋ช ์ธ(javax.persistence.Entity)์ด๊ธฐ์ ์คํ๋ง ์์กด์ฑ์ด ์๋๋ผ๋ ์ฌ์ฉํ ์ ์๊ธดํ๋ค. ์๋ Hybernate ์์ ์๋ ์๋์ ๊ฐ์ด ์ค์ ์ ํด์ฃผ์๋ค.
<property name="packagesToScan">myNamespace.*</property>
์ด ์ค์ ๋ํ @SpringbootApplication ์ ์๋ @EnableAutoConfiguraion์ด ์๋์ผ๋ก ํด์ค๋ค.
๋ง์ฝ @Entity๊ฐ ๋ค๋ฅธ ๋ชจ๋(๋๋ ์ธ๋ถ JAR)์ ์์ด ์ค์บ์ด ์๋๋ ๊ฒฝ์ฐ, ์๋์ ๊ฐ์ด ์ง์ ์ง์ ํด์ค ์ ์๋ค.
๊ฐํน @ComponetScan๊ณผ ๋์ผํ ๊ธฐ๋ฅ์ด๋ผ๊ณ ์๊ฐํ๋ ์ฌ๋์ด ์๋๋ฐ, ์ปดํฌ๋ํธ ์ค์บ์ ๋ง ๊ทธ๋๋ก @Component๊ฐ ๋ฌ๋ฆฐ ํด๋์ค๋ฅผ ์คํ๋ง ๋น์ผ๋ก ๋ฑ๋กํ๋ ๊ธฐ๋ฅ์ด๋ค. @EntityScan์ Entity๋ฅผ ์ฐพ๋ ๋ฒ์๋ฅผ ํ์ฅํ ๋ฟ, ์คํ๋ง ๋น๊ณผ๋ ์๋ฌด๋ฐ ์ฐ๊ด์ด ์๋ค. ๋น์ ๋ง๋ค์ง ์๋๋ค.
// ๋ ๋ค ์ค์ ํด์ค ๋๋ ์ด๋ ๊ฒ ๋ฐ๋ก ์ฌ์ฉํ๋๊ฒ ์ผ๋ฐ์ ์ด๋ค.
@ComponentScan("org.example.base")
@EntityScan("org.example.base.entities")
public class MyConfig {
}
Spring Data JPA๋ ์ฌ๊ธฐ์ ์ถ๊ฐ์ ์ผ๋ก @Repository๊ฐ ๋ถ์ด์๋ ํด๋์ค๋ฅผ ์ฐพ์ ํ๋ก์๋ก ๋์ฒดํด์ค๋ค.
@Repository // org.springframework.data.repository.Repository
SpringDataJpa๋ฅผ ์ฌ์ฉํ๋ค๋ฉด EntityManagerFactory๋ ์คํ๋ง ์ปจํ ์ด๋์ ์์ฑ๋์ด ์๋ค.
์ฌ์ฉํ ์ผ์ ์๊ฒ ์ง๋ง, ๋ง์ฝ ์คํ๋ง ์ปจํ ์ด๋์ ์๋ EntityMangerFactory์์ EntityManager๋ฅผ ๋ฐ๊ณ ์ถ๋ค๋ฉด ์๋์ ๊ฐ์ด ์ด๋ ธํ ์ด์ ์ ์ด์ฉํด์ ์ฃผ์ ๋ฐ์ผ๋ฉด ๋๋ค.
@PersistenceContext // ์ต์ ๋ฒ์ ์คํ๋ง์์๋ @Autowired๋ก ์ฌ์ฉํด๋ ๋๋ค.
EntityManager entityManager;
// EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("emf name");
// EntityManager entityManager = entityManagerFactory.createEntityManager();
๊ทธ๋ฆฌ๊ณ ๊ณต์ฉ ์ธํฐํ์ด์ค์ธ JpaRepository๋ฅผ ์ฌ์ฉํ๋ค๋ฉด, ์๋ฌด๋ฐ ์ถ๊ฐ ์์ ์์ด ์๋์ฒ๋ผ ์ฌ์ฉํ๋ฉด ๋๋ค.
// ์ด๊ฒ ๋. ์๋ตํ๊ฒ ์๋๋ผ ๊ทธ๋ฅ ์ธํฐํ์ด์ค๋ง ๋๋ฉด ๋์ด๋ค.
public interface MemberRepository extends JpaRepository<Member, Long> {
}
๐ญ ๊ธฐ๋ณธ ์ฌ์ฉ๋ฐฉ๋ฒ
๋ค๋ง, interface JpaRepository<Entity, ID>๋ฅผ ์ฌ์ฉํ๋ค๋ฉด ์๋ฌด๊ฒ๋ ํ์ง ์์๋ ๋๋ค.
ํด๋น ์ธํฐํ์ด์ค์ ๊ตฌํ์ฒด๋ฅผ ๋ง๋คํ์๋, @Repository๋ฅผ ๋ถ์ผ ํ์๋ ์๋ค.
// ์ด๊ฒ ๋. ์๋ตํ๊ฒ ์๋๋ผ ๊ทธ๋ฅ ์ธํฐํ์ด์ค๋ง ๋๋ฉด ๋์ด๋ค.
// SpringDataJpa์์ ๋ง๋ ๊ธฐ๋ณธ Jpa ๊ตฌํ์ฒด๊ฐ ์คํ๋ง ์ปจํ
์ด๋์ ์์ฑ๋์ด์๊ธฐ ๋๋ฌธ์ด๋ค.
public interface MemberRepository extends JpaRepository<Member, Long> {
}
๋ณต์กํ ์กฐํ๋ ์๋๊ณ ๋ฉ์๋ ์ด๋ฆ๋ง ๋ณด๋ฉด ๋ฑ ๋ฌด์จ ์ฟผ๋ฆฌ์ธ์ง ๋ณด์ด๋๋ฐ..๊ทธ๋ฅ ์์์ ๋ง๋ค์ด์ฃผ๋ฉด ์๋๋?
โก ์ด๊ฒ ๋ฐ๋ก SpringDataJpa์ ๊ธฐ๋ฅ์ด๋ค.
JpaRepository ์ธํฐํ์ด์ค์๋ ๋น์ฆ๋์ค์ ์์ฃผ์ฐ๋ ์ฝ๋ (save, findById, delete..)๊ฐ ๋๋ถ๋ถ ํฌํจ๋์ด์๋ค.
๊ทธ ์ธ ํน์ ํ๋๋ช ์ด ๋ค์ด๊ฐ๋ ๊ฒฝ์ฐ(findByEmail ๋ฑ)๋ง ๋ฐ๋ก ์ถ๊ฐ๋ก ์ธํฐํ์ด์ค๋ฅผ ์์ฑํด์ฃผ๋ฉด ๋๋ค.
๋ง์ด ๋๋ถ๋ถ์ด์ง, JPA๋ฅผ ์ฌ์ฉํด์ ๊ฐ๋ฐ์๊ฐ ๋ง๋ค ์ ์๋ ๋ชจ๋ ๋ฉ์๋๋ฅผ ๊ฐ์ง๊ณ ์๋ค.
๋ฌผ๋ก ์กฐ๊ฑด์ด ์ฌ๋ฌ๊ฐ๊ฑฐ๋ ์ฟผ๋ฆฌ๊ฐ ๋ณต์กํด์ง๋ฉด QureyDSL๋ฑ์ผ๋ก JPQL์ ์ง์ ์์ฑํ๊ฑฐ๋, ๊ทธ๋ฅ JPA๋ฅผ ์์ฐ๋ ๊ฒฝ์ฐ๋ ๋ง๋ค.
repository.save(new Member("username", 10));
repository.save(new Member("username", 20));
// ๋ฉ์๋ ์ด๋ฆ ๊ท์น๋ง ์ ์งํจ๋ค๋ฉด, ์ด๋ฐ ๊ฒ๋ ๊ฐ๋ฅํ๋ค.
List<Member> result = repository.findByUsernameAndAgeGreaterThan("username", 15);
assertThat(result.size()).isEqualTo(1);
Member member = result.get(0);
assertThat(member.getUsername()).isEuqalTo("username");
assertThat(member.getAge()).isEqualTo(20);
๋ฉ์๋ ์ด๋ฆ ๊ท์น (Count, Between, In๋ฑ ์ ๋งํ๊ฑด ๋ค ์ง์ํ๋ค)_
๋ฌธ์์ด LIKE ๋ฅผ Spring JPA ๋ฉ์๋๋ค์ด๋ฐ์ผ๋ก ๋ง๋ค๊ธฐ
public interface BookRepository extends JpaRepository<Book, Long>{
// select ... like %:title%
List<Book> findBooksByTitleContaining(String title);
List<Book> findBooksByTitleContains(String title);
List<Book> findBooksByTitleIsContaining(String title);
// select ... like :title
List<Book> findBooksByTitleLike(String title);
// select ... like :title%
List<Book> findBooksByTitleStartingWith(String title);
// select ... like %:title
List<Book> findBooksByTitleEndingWith(String title);
}
๐ญ ์ด๊ฒ ์ด๋ป๊ฒ ๊ฐ๋ฅํ๊ฑฐ์ฃ ? -> SimpleJpaRepository
SpringDataJPA๋ ์ธํฐํ์ด์ค๋ฅผ ๋ฐํ์ผ๋ก class SimpleJpaRepository๋ฅผ ์คํ๋ง ๋น์ผ๋ก ์ปจํ
์ด๋์ ๋ฑ๋ก์ํจ๋ค.
์ฆ interface JpaRepository ์ธํฐํ์ด์ค์ ์๋ง์ SimpleJpaRepository๊ฐ ์ปจํ
์ด๋์ ์ํด ์ฃผ์
๋๋ค.
- [ JDBC๋ JPA์ ๋ฐ์ํ ์์ธ ]๋ฅผ [ ์คํ๋ง์ด ์ ๊ณตํ๋ ์์ธ ]๋ก ํฌ์ฅ์์ผ์ ๋ฐํํ๋ค.
- ์ธํฐํ์ด์ค์ ์ ๋ค๋ฆญ (Entity, ID) ํ์ ์ ๋ฐํ์ผ๋ก JPA ์ฟผ๋ฆฌ๋ฌธ(em.persist ๋ฑ)์ ์์ฑํ๋ค.
- ํน์ ํ๋๋ฅผ ์ด์ฉํ ์ฝ๋( findByUsername, findByEmail ) ๋ํ ๋ฉ์๋ ๋ช ์ ๋ถ์ํ์ฌ JPQL ์ฟผ๋ฆฌ๋ฅผ ์์ฑํ๋ค.
์ค์ ๋ก SimpleJpaRepository๊ฐ ์ฐ๋ QureyUtils ๋ฅผ ์ดํด๋ณด๋ฉด, ์ฟผ๋ฆฌ๋ฌธ์ ๋ํ ์ ๊ทํํ์์ด ์ ์๋์ด ์๋ค.
public abstract class QueryUtils {
public static final String COUNT_QUERY_STRING = "select count(%s) from %s x";
public static final String DELETE_ALL_QUERY_STRING = "delete from %s x";
public static final String DELETE_ALL_QUERY_BY_ID_STRING = "delete from %s x where %s in :ids";
private static final String IDENTIFIER = "[._$[\\P{Z}&&\\P{Cc}&&\\P{Cf}&&\\P{Punct}]]+";
static final String COLON_NO_DOUBLE_COLON = "(?<![:\\\\]):";
static final String IDENTIFIER_GROUP = String.format("(%s)", IDENTIFIER);
... // ์๋ต
}
๐ ํธ๋์ญ์ ์ ์ด๋ป๊ฒํ์ฃ ? ์์ด๋ ๋์ํ๋๋ฐ?
์ฐ์ , JPA์ ๋ชจ๋ ๋ณ๊ฒฝ์ ํธ๋์ญ์ ์์์๋ง ๋์ํ๋ค. (์์ผ๋ฉด ์์ธ๊ฐ ๋ฐ์ํ๋ค.)
public static void main(String[] args) {
/* ์ดํ๋ฆฌ์ผ์ด์
์ด ์์ํ ๋ Factory ์์ฑํ๋ค. */
EntityManagerFactory factory = Persistence.createEntityManagerFactory("hello");
// ๐ญ 1. ์ค๋ ๋์์ ์ฌ์ฉํ EntityManager ๋ฐ์์จ๋ค. ํ ํธ๋์ญ์
์ ํ EntityManager๊ฐ ์ฌ์ฉ๋๋ค.
EntityManager entityManager = factory.createEntityManager();
// ๐ญ 2. JPA๋ฅผ ์ด์ฉํ ๋ชจ๋ ๋ฐ์ดํฐ ๋ณ๊ฒฝ์ ํธ๋์ญ์
์์์๋ง ์คํ๋ ์ ์๋ค.
EntityTransaction tx = entityManager.getTransaction();
try {
Member member = new Member();
member.setId(2L);
member.setName("Kim");
entityManager.persist(member);
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
// ๐ญ 3. ์ฌ์ฉ์ด ๋๋ EntityManager ๋ฐ๋ฉํ๋ค.
entityManager.close();
}
factory.close(); /* ์ดํ๋ฆฌ์ผ์ด์
์ด ์ข
๋ฃํ ๋ ํ์์์ด์ง Factory ๋ฐํํ๋ค. */
}
SimpleJpaRepository ์ฝ๋๋ฅผ ๋ณด๋ฉด, ํด๋์ค ์์ฒด์ @Transactional์ด ๊ฑธ๋ ค์๋ค.
์๋น์ค ๊ณ์ธต์์ ํธ๋์ญ์ ์ ์์ํ์ง ์๋๋ค๋ฉด Repository์์ ํธ๋์ญ์ ์ด ์์ํ๋ค.
(* propagation.Required ์ต์ ์ด ๊ธฐ๋ณธ๊ฐ์ด๊ธฐ ๋๋ฌธ. ์ด๋ ํธ๋์ญ์ ์ด ์๋ค๋ฉด ๊ทธ๋๋ก ์ฐ๊ณ , ์์ผ๋ฉด ์๋ก ์์ํ๋ค. )
Spring Data ํ๋ก์ ํธ์ ์๋ธ ํ๋ก์ ํธ๊ฐ Spring Data JPA์ด๋ค.
๊ทธ๋์ ์๋์ ๊ฐ์ด ๊ณตํต๋ถ๋ถ์ ๋ฝ์ ์์ ๊ด๊ณ๋ฅผ ๋ง๋ค์ด๋๋๋ฐ, ์์ธํ๊ฒ ์ ํ์๋ ์๊ธดํ๋ค.
๊ทธ๋ฆฌ๊ณ ์ถ์ํ๋ฅผ ์ ์์ผ๋์ ์ฌ์ฉ์ ํธํ๊ฒ ๋ง๋ค์์ ๋ฟ, ๋ด๋ถ์ฝ๋๋ JPA๋ฅผ ์ฌ์ฉํด์ ๊ตฌํํ๋ค.
JPA๋ ๊ฒฐ๊ตญ Java์ JDBC๋ฅผ ์ด์ฉํด์ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ์ด์ฉํจ์ ์์ง๋ง์.
๋ญ๋ ์ง ์์์ ์ฒ๋ฆฌํด์ฃผ๋ ๋ง๋ฒ์ ๋๊ตฌ๊ฐ ์๋๋ค.
๐ญ ์คํ๋ง์์์ NameQurey
์ด ๊ธฐ๋ฅ์ JPA์ @NameQurey๋ฅผ ํ์ฉํ๋ค. (์ง์ ์ฌ์ฉํ ์ผ์ ์์ผ๋ ์ค๋ช ๋ง ๋ฃ๊ณ ๊ฐ์)
์ด๋ ์ํฐํฐ์ ํน์ ์ฟผ๋ฆฌ๋ฅผ ํ ๋นํด๋๊ณ , ํ์ํ ๋ ๊บผ๋ด์ ์ธ ์ ์๋ ๊ธฐ๋ฅ์ด๋ค.
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@NamedQuery(
name = "Member.findByUsername",
query="select m from Member m where m.username = :username" // :username ๋ ํ๋ผ๋ฉํ
)
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
...
}
@PersistenceContext
EntityManager em;
public List<Member> findByUsername(String username) {
// createNameQurey()๋ฅผ ํตํด์ ๊บผ๋ด ์ธ ์ ์๋ค.
return em.createNamedQuery("Member.findByUsername", Member.class)
.setParameter("username", "์ด๋ฆ")
.getResultList();
}
๋ฌผ๋ก ์๋์ ๊ฐ์ด ์ฟผ๋ฆฌ๋ฅผ ์ง์ ์์ฑํด๋ ๋๊ฐ์ด ๋์ํ๋ค.
์ด๋ ์ฌ์ฌ์ฉ์ฑ ์ธก๋ฉด์๋ ์ข์ง๋ง, NameQurey๋ ๋น๋ํ๋ ์์ ์ ์ฟผ๋ฆฌ์ ์ ํจ์ฑ์ ๋ฐ๋ก ํ์ธํ ์ ์๋ค๋ ์ฅ์ ๋ ์๋ค.
public List<Member> findByUsername(String username) {
// ์ด๋ ๊ฒ ์ฟผ๋ฆฌ๋ฅผ ์ง์ ์์ฑํด๋ ๋์ง๋ง, ์ด๋ ์คํํ๋ ์์ (๋ฐํ์)์ ์ฟผ๋ฆฌ ์ ํจ์ฑ์ ํ๋ณํ๋ค.
return em.createQuery("select m from Member m where m.username = :username")
.setParameter("username", "์ด๋ฆ")
.getResultList();
}
์คํ๋ง ๋ฐ์ดํฐ JPA์์๋ ์๋์ ๊ฐ์ด @Query๋ฅผ ์ด์ฉํด์ NameQurey๋ฅผ ์ฌ์ฉํ ์ ์๋ค.
public interface JpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {
@Query(name = "Member.findByUsername")
List<Member> findByUsername(@Param("username") String username);
}
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@NamedQuery(
name = "Member.findByUsername",
query="select m from Member m where m.username = :username" // :username ๋ ํ๋ผ๋ฉํ
)
public class Member {
...
}
์ฌ๋ฐ๋์ ์ @Qurey๋ฅผ ์๋ตํ๋๋ผ๋ SpringDataJPA๋ NameQurey๋ฅผ ๋จผ์ ์ฐพ์๋ณด๊ณ , ์์ ๋ ๊ธฐ๋ณธ ๊ฐ์ ์ฌ์ฉํ๋ค.
๋ค๋ง ์ด๋ฐ ๋ฐฉ๋ฒ์ ๊ถ์ฅํ์ง๋ ์๊ณ , ์ ์ด์ @NameQurey ์์ฒด๋ ์ค๋ฌด์์๋ ๊ฑฐ์ ์ฌ์ฉํ์ง ์์ผ๋ ๋์ด๊ฐ๋๋ก ํ์.
๐ญ @Query(...)
@NameQuery๋ ์ค๋ฌด์์ ๊ฑฐ์ ์ฌ์ฉํ์ง ์๊ณ , ๋ณดํต์ ์๋์ ๊ฐ์ด ๋ฆฌํฌ์งํ ๋ฆฌ์ ์ฟผ๋ฆฌ๋ฅผ ์ง์ ์์ฑํ๋ค.
public interface JpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {
@Query("select m from Member m where m.username = :username")
List<Member> findByUsername(@Param("username") String username); // username ์ผ๋ก ์กฐํ
@Qurey("select m from Member m where m.username in :names")
List<Member> findByNames(@Param("names") List<String> names); // ์ปฌ๋ ์
์ผ๋ก in ์กฐํ
// @Query๋ฅผ ์ฐ๋ฉด ์ด๋ ๊ฒ ๊ธด ์ด๋ฆ์ ๋ฉ์๋๋ฅผ ์ฌ์ฉํ์ง ์์๋ ๋๋ค.
List<Member> findByUsernameAndAgeGraterThanAnd...(...);
}
์ด๋ @NameQuery์ ๋๊ฐ์ด ์๋ํ๋ค. ์ฑ ์คํ์์ ์ ์ฟผ๋ฆฌ ์ค๋ฅ๋ฅผ ๊ฒ์ฆํ๋ค.
๋ง์ฝ Entity๊ฐ ์๋๋ผ DTO๋ก ์กฐํํ๊ณ ์ถ์ ๋์๋, ์๋์ ๊ฐ์ด ์ฌ์ฉํ ์ ์๋ค.
public interface JpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {
@Query("select new mypackage.dto.MemberDto(m.id, m.username, t.name) from Member.m join m.team t")
List<MemberDto> findMemberDto();
}
@Data
public final class MemberDto {
private Long id;
private String username;
private String teamName;
// JPQL ์ฟผ๋ฆฌ์ ์์ฑ์๋ฅผ ์ฌ์ฉํ๋ฏ๋ก, ์์ฑ์๋ ํ์.
public MemberDto(Long id, String username, String teamName) {
this.id = id;
this.username = username;
this.teamName = teamName;
}
}
์ฌ์ง์ด ์๋์ ๊ฐ์ด ์ ๋ค๋ฆญ๋ ์ง์ํ๋ค.
๋ง์ง๋ง์ผ๋ก ๋์ ์ฟผ๋ฆฌ๋ ๋ฌธ์์ด๋ก ์ ๋ ฅํ๊ธฐ์๋ ๋๋ฌด ๊น๋ค๋กญ๋ค. ๊ทธ๋์ ๋ณดํต ์ฟผ๋ฆฌ๋น๋(QueryDSL๋ฑ)์ ํ์ฉํ๋ค.
์ฌ์ค ์์์ ์ธ๊ธํ ๋ฐฉ๋ฒ๋ค๋ QueryDSL์ ์ฌ์ฉํ๋ฉด ํจ์ฌ ๊ฐํธํ๊ฒ ๋ง๋ค ์ ์๊ธดํ๋ค.
๋์ ์ฟผ๋ฆฌ๊ฐ..๋ญ์์ฃ ?
์์ฑํ ์ฟผ๋ฆฌ๊ฐ ์ด๋ ํ ์ํฉ์์๋ ๋ณ๊ฒฝ๋์ง ์๋ ์ฟผ๋ฆฌ๊ฐ ์ ์ ์ฟผ๋ฆฌ๊ฐ ๋ ๊ฒ์ด๊ณ ์
๋ ฅ๊ฐ์ด๋ ํน์ ์ํฉ์ ๋ฐ๋ผ ์ฟผ๋ฆฌ๋ฌธ์ด ๋ณ๊ฒฝ๋ ์ ์๋ ์ฟผ๋ฆฌ๊ฐ ๋์ ์ฟผ๋ฆฌ์ด๋ค.
๋์ ์ฟผ๋ฆฌ
- ์ฝ๋ ์คํ ์์ ์ SQL ์ฟผ๋ฆฌ๋ฌธ์ด ๋์ ์ผ๋ก ๊ตฌ์ฑ๋๊ณ ์คํ๋๋ ์ฟผ๋ฆฌ
- Stored Procedure๋ฅผ ์์ฑํ ๋ ํ์์ ๋ฐ๋ผ ๋์ ์ฟผ๋ฆฌ๋ก ์์ฑ
- ๋์ ์ฟผ๋ฆฌ ์คํ SQL ๋ฌธ์์ด์ ๊ฐ์ด ๋ฐ๋๋ ๋ณ์๋ฅผ ๋ฃ์ด ์ฟผ๋ฆฌ๋ฌธ์ ์์ฑํ๊ฒ ๋๋ฉด SP๊ฐ ์๋ก ์บ์ฑ๋์ด ์ฌ์ฌ์ฉ์ฑ์ ๋จ์ดํธ๋ฆผ
- ๋์ ์ฟผ๋ฆฌ๋ EXEC()ํจ์๋ฅผ ์ฌ์ฉํ๊ฑฐ๋ SP_EXECUTESQL์ ์ฌ์ฉํ์ฌ ์คํํ๋ค.
์ ์ ์ฟผ๋ฆฌ
- ์ผ๋ฐ์ ์ธ ์ฟผ๋ฆฌ
- ๋ณ์์ ๋ฌธ์์ด์ ๋์ ํ์ฌ ์ฟผ๋ฆฌ๋ฌธ์ ์์ฑํ๋ ๊ฒ์ด ์๋ ์ผ๋ฐ์ ์ผ๋ก ์์ฑ๋ SQL ์ฟผ๋ฆฌ
- ์ ์ ์ฟผ๋ฆฌ๋ก ์์ฑ๋ Stored Procedure๋ ์๋ก ์บ์ฑ๋์ง ์์ ์ฌ์ฌ์ฉ์ฑ์ ๋จ์ดํธ๋ฆฌ์ง ์๋๋ค.
CREATE PROCEDURE TestSP
-- Add the parameters for the stored procedure here
@input NVARCHAR(50) =null,
@output INT =0 OUT
AS
-- sp_executesql์ ์คํํ ๋ณ์ ์ ์ธ
DECLARE @Sql NVARCHAR(MAX) = ''
DECLARE @Where NVARCHAR(MAX) = ''
DECLARE @Param NVARCHAR(MAX) = ''
-- ์
๋ ฅ ์ด๋ฆ์ด ๋ค์ด์ค๋ฉด WHERE์ ์ถ๊ฐ
IF ISNULL(@input,'') <> '' BEGIN
SET @Where = @Where + N'
WHERE NAME = @input '
END
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
SET @Sql = @Sql + N'
SELECT @output = AGE
FROM dbo.testTable '
SET @Sql = @Sql + @Where
SET @Param = N'
@input NVARCHAR(50),
@output INT OUT '
EXEC SP_EXECUTESQL @Sql
,@Param
,@input = @input
,@output = @output
๐ญ ๋ฐํ ํ์
์ฐธ๊ณ ๋ก SpringDataJpa๋ ๋ค์ํ ๋ฐํ ํ์ ์ ์ง์ํ๋ค.
๋ณดํต์ ์๋์ 3๊ฐ์ง. ์ปฌ๋ ์ , ๊ฐ์ฒด, Optional<๊ฐ์ฒด> ๋ฅผ ๋ง์ด ์ฌ์ฉํ๋ค.
List<Member> findByUsername(String name); //์ปฌ๋ ์
Member findByUsername(String name); //๋จ๊ฑด
Optional<Member> findByUsername(String name); //๋จ๊ฑด Optional
์ปฌ๋ ์ ์ ๊ฐ์ด ์์ด๋ ์์ธ๊ฐ ๋ฐ์ํ์ง๋ ์๊ณ , ๋น ์ปฌ๋ ์ ์ ๋ฐํํ๋ค.
๋จ๊ฑด ์กฐํ์ธ๋ฐ ๋ง์ฝ ๊ฒฐ๊ณผ๊ฐ 2๊ฑด ์ด์์ด๋ฉด ์์ธ(javax.NonUniqueResultException)์ด ๋ฐ์ํ๋ค.
๋จ๊ฑด ์กฐํ์ ๊ฒฐ๊ณผ๊ฐ ์์ผ๋ฉด null์ด๋ Optional.Empty๋ฅผ ๋ฐํํ๋ค.
๋จ๊ฑด ์กฐํ์ ๋ด๋ถ ๋์์ JPA์ Query.getSingleResult()๋ฅผ ํธ์ถํ๋๋ฐ, ํด๋น ๋ฉ์๋๋ `javax.NoResultException` ์์ธ๋ฅผ ๋ฐ์์ํจ๋ค. ๋จ๊ฑด ์กฐํ์ ๊ฒ์๊ฒฐ๊ณผ๊ฐ ์๋๊ฑด ์์ธ ์ํฉ์ด ์๋๋ผ, ์ถฉ๋ถํ ์์ ์ ์๋ ์ํฉ์ด๋ค. ๊ทธ๋์ ๊ฐ๋ฐ์ ์ ์ฅ์์ ๋งค์ฐ ๋ฒ๊ฑฐ๋ก์ด๋ฐ, ์คํ๋ง์์๋ ์ด ์์ธ๋ฅผ ์ก์ null๋ก ๋ฐํํ๋๋ก ์ฒ๋ฆฌํ๊ณ ์๋ค.
์์ฃผ ์ฌ์ฉํ๋๊ฒ ์์ 3๊ฐ์ง๋ผ๋๊ฑฐ์ง, ๊ฑฐ์ ๋๋ถ๋ถ์ ๋ฐํํ์ ์ SpringDataJPA์์๋ ์ ๊ณตํ๊ณ ์๋ค.
void, ๊ธฐ๋ณธํ์
(int, long), Mono<T>, Flux<T>, Maybe<T>, Future<T>, Stream<T>, Iterator<T>, Map<K,V>..
๐ญ ํ์ด์ง
๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ํ์ด์ง์ ์ธ์ ๋ ๊ณจ์น์ํ ๋ฐ๋ณต ์์ ์ด์๋ค. ๋งค๋ฒ ๊ตฌ๊ธ๋ง์ ํตํด ์ต์ ํ๋ ํ์ด์ง SQL์ ์ฐพ์์ผํ๋ค.
์์ JPA๋ ํ์ด์ง์ ๊ฐ๋จํ๊ฒ ๋ง๋ค์ด์คฌ์๋ค.
public List<Member> findByPage(int age, int offset, int limit) {
// ๋ฑ๋ก๋ DBMS ์ ์ต์ ํ๋ ์ฟผ๋ฆฌ๋ฅผ ์์ฑํ๋ค.
return em.createQuery("select m from Member m where m.age = :age order by m.username desc")
.setParameter("age", age)
.setFirstResult(offset) // ์์์์น
.setMaxResults(limit) // ์ต๋๊ฐ์
.getResultList();
}
public long totalCount(int age) {
return em.createQuery("select count(m) from Member m where m.age = :age ", Long.class)
.setParameter("age", age)
.getSingleResult();
}
์คํ๋ง ๋ฐ์ดํฐ JPA์์๋ ํ์ด์ง ์ฟผ๋ฆฌ๋ฅผ Sort ์ Pageable ์ธํฐํ์ด์ค๋ก ํ์คํ ์์ผฐ๋ค.
// Page, Slice, Sort, Pageable ๋ฑ์ org.sptringframework.data.domain.* ์ ์กด์ฌํ๋ค.
Page<Member> findByUsername(String name, Pageable pageable); //count ์ฟผ๋ฆฌ ์ฌ์ฉ
Slice<Member> findByUsername(String name, Pageable pageable); //count ์ฟผ๋ฆฌ ์ฌ์ฉ์ํจ
List<Member> findByUsername(String name, Pageable pageable); //count ์ฟผ๋ฆฌ ์ฌ์ฉ์ํจ
List<Member> findByUsername(String name, Sort sort); // ์ ๋ ฌ
๋ํ Page์ Slice๋ผ๋ ๋ฐํ ํ์ ์ ์ ๊ณตํด์ค๋ค.
๋ฐํ ํ์ ์ Page๋ Slice๋ก ๋ฐ๋๋ค๋ฉด ์ถ๊ฐ์ ์ผ๋ก ํ์ํ ๊ฐ๋ค(totalCount ๋ฑ)์ ์ฟผ๋ฆฌ๋ก ๋ ๋ฆฐ๋ค.
- ์ปฌ๋ ์ (List)๋ก ๋ฐ์ผ๋ฉด ๊ทธ๋ฅ ๊ฒฐ๊ณผ๋ง ์ปฌ๋ ์ ์ ๋ฐ์์จ๋ค.
- Page ๋ Slice๋ฅผ ์์๋ฐ์ ๋ง๋ ๊ฐ์ฒด๋ก, ๊ฒฐ๊ณผ์ count ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ๋ฅผ ํฌํจํ๋ค.
- Slice ๋ ๊ฒฐ๊ณผ์ count ์ฟผ๋ฆฌ ์์ด ๋ค์ํ์ด์ง๋ง ํ์ธํ๋ค. (limit+1 ์กฐํ, ํ์ด์ง ๋ฒํธ๊ฐ ํ์์๋ ๊ฒฝ์ฐ. ๋๋ณด๊ธฐ๋ฒํผ)
@Test
public void page() throws Exception {
//given
repoistory.save(new Member("member1", 10L)); // Member(name, code)
repoistory.save(new Member("member2", 10L));
repoistory.save(new Member("member3", 10L));
repoistory.save(new Member("member4", 10L));
repoistory.save(new Member("member5", 10L));
//when
PageRequest pageRequest = PageRequest.of(// page, size, Sort ๊ตฌํ์ฒด
0, 3, Sort.by(Direction.DESC, "username"));
Page<Member> page = repoistory.findByCode(10L, pageRequest);
//then
List<Member> content = page.getContent(); //์กฐํ๋ ๋ฐ์ดํฐ
assertThat(content.size()).isEqualTo(3); //์กฐํ๋ ๋ฐ์ดํฐ ์
assertThat(page.getTotalElements()).isEqualTo(5); //์ ์ฒด ๋ฐ์ดํฐ ์
assertThat(page.getNumber()).isEqualTo(0); //ํ์ด์ง ๋ฒํธ
assertThat(page.getTotalPages()).isEqualTo(2); //์ ์ฒด ํ์ด์ง ๋ฒํธ
assertThat(page.isFirst()).isTrue(); //์ฒซ๋ฒ์งธ ํญ๋ชฉ์ธ๊ฐ?
assertThat(page.hasNext()).isTrue(); //๋ค์ ํ์ด์ง๊ฐ ์๋๊ฐ?
}
Pageable์ ์ฌ์ฉํ ๋์๋ org.springframework.data.domain์ ์๋ PageRequest๋ฅผ ๊ตฌํ์ฒด๋ก ์ฌ์ฉํ๋ค.
PageRequest.of( offset, limit )๋ฅผ ์ ๋ฌํ๋ฉด ๋๋ค. ํ์ํ๋ค๋ฉด 3๋ฒ์งธ ์ธ์๋ก Sort ๋ฅผ ์ด์ฉํด ์ ๋ ฌ์ ๋ณด๋ฅผ ์ ๊ณตํด์ค ์ ์๋ค.
//when
PageRequest pageRequest = PageRequest.of( 0, 3, Sort.by(Direction.DESC, "username") );
Page<Member> page = repoistory.findByCode(10L, pageRequest);
์ด๋ฅผ ์ด์ฉํด Page<Entity>๋ฅผ ์ฝ๊ฒ ๋ฐ์์ฌ ์ ์๋ค.
๋ง์ฝ ์ ๋ ฌ ๊ธฐ์ค์ด ๋๋ฌด ๋ณต์กํด์, Sort๋ก ์ฒ๋ฆฌํ๊ธฐ ๊น๋ค๋กญ๋ค๋ฉด ๊ทธ๋ฅ @Query(...)๋ฅผ ์ด์ฉํด์ ์ง์ ์์ฑํ๋๊ฑธ ๊ถ์ฅํ๋ค.
์ฐธ๊ณ ๋ก Page ๋ Slice๋ฅผ ์์๋ฐ์ ๊ตฌํ๋์ด์๋๋ฐ, ์ค์ ์ฝ๋๋ฅผ ์ดํด๋ณด๋ฉด ์๋์ ๊ฐ๋ค.
public interface Page<T> extends Slice<T> {
int getTotalPages(); //์ ์ฒด ํ์ด์ง ์
long getTotalElements(); //์ ์ฒด ๋ฐ์ดํฐ ์
<U> Page<U> map(Function<? super T, ? extends U> converter); //๋ณํ๊ธฐ
}
public interface Slice<T> extends Streamable<T> {
int getNumber(); //ํ์ฌ ํ์ด์ง
int getSize(); //ํ์ด์ง ํฌ๊ธฐ
int getNumberOfElements(); //ํ์ฌ ํ์ด์ง์ ๋์ฌ ๋ฐ์ดํฐ ์
List<T> getContent(); //์กฐํ๋ ๋ฐ์ดํฐ
boolean hasContent(); //์กฐํ๋ ๋ฐ์ดํฐ ์กด์ฌ ์ฌ๋ถ
Sort getSort(); //์ ๋ ฌ ์ ๋ณด
boolean isFirst(); //ํ์ฌ ํ์ด์ง๊ฐ ์ฒซ ํ์ด์ง ์ธ์ง ์ฌ๋ถ
boolean isLast(); //ํ์ฌ ํ์ด์ง๊ฐ ๋ง์ง๋ง ํ์ด์ง ์ธ์ง ์ฌ๋ถ
boolean hasNext(); //๋ค์ ํ์ด์ง ์ฌ๋ถ
boolean hasPrevious(); //์ด์ ํ์ด์ง ์ฌ๋ถ
Pageable getPageable(); //ํ์ด์ง ์์ฒญ ์ ๋ณด
Pageable nextPageable(); //๋ค์ ํ์ด์ง ๊ฐ์ฒด
Pageable previousPageable();//์ด์ ํ์ด์ง ๊ฐ์ฒด
<U> Slice<U> map(Function<? super T, ? extends U> converter); //๋ณํ๊ธฐ
}
Slice๋ count ์ฟผ๋ฆฌ๋ฅผ ๋ ๋ฆฌ์ง ์๊ณ , ์ต๋ ๊ฐ์(limit)๋ฅผ ๋ค ๊ฐ์ ธ์๋ค๋ฉด ๋ค์ํ์ด์ง๊ฐ ์๋ ๊ฑธ๋ก ํ๋จํ๋ค.
ํ ์ด๋ธ์ ๋ฐ์ดํฐ๊ฐ ๋ง์์ totalCount๋ฅผ ์ธ๋ ์ฟผ๋ฆฌ๊ฐ ๋ถ๋ด์ค๋ฝ๋ค๋ฉด, Slice๋ฅผ ๋์ ์ฌ์ฉํด์ ์ฟผ๋ฆฌ๋ฅผ ์ต์ ํ ํ ์ ์๋ค.
Page์ Slice์ ์๋ map ๋ฉ์๋๋ ๋ค๋ฅธ ๊ฐ์ฒด๋ก ๋งคํํ ๋ ์ฌ์ฉํ๋ค.
์๋์ ๊ฐ์ด ๋๋คํจ์๋ฅผ ์ด์ฉํด์ DTO์ ์ฝ๊ฒ ๋งคํํ ์ ์๋ค.
PageRequest pageRequest = PageRequest.of( 0, 3 );
Page<Member> page = repository.findByAge(age, pageReuqest);
Page<MemberDto> toMap = page.map(member -> new MemberDto(member.getId(), member.getUsername()));
๐ญ @Modify, ๋ฒํฌ์ฑ ์์ ์ฟผ๋ฆฌ
์์ JPA์ ๋ฒํฌ์ฑ ์ฟผ๋ฆฌ(executeUpdate)๋ฅผ ์ดํดํด์ผ ์คํ๋ง์ @Modify๋ฅผ ์ดํดํ ์ ์๋ค.
JPA๋ ์ปค๋ฐ ์์ ์ Entity์ ๋ณ๊ฒฝ์ ๊ฐ์งํด์ ์ ์ ํ ์ฟผ๋ฆฌ๋ฅผ ๋ฐ์์ํจ๋ค.
ํ์ง๋ง ์์ ์ ํ๋ค๋ณด๋ฉด ๋ฒํฌ์ฑ ์ฟผ๋ฆฌ(ex ์ฌ๊ณ ๊ฐ 10๊ฐ ๋ฏธ๋ง์ธ ์ํ ์ ์ฒด์ ๊ฐ๊ฒฉ์ 20% ์์น) ์์ ์ ํ ๋๋ ์๋ค.
SQL๋ก๋ ๊ฐ๋จํ ์์ ์ด์ง๋ง JPA ๋ณ๊ฒฝ๊ฐ์ง๋ก๋ ๋ฆฌ์์ค๋ ๋ง์ด ์ก์๋จน๊ณ , ์ฟผ๋ฆฌ๊ฐ ๋ณ๊ฒฝ๋ ๊ฐ์๋งํผ ๋๊ฐ ์ ์๋ค.
๊ทธ๋์ ์์ JPA์์๋ ์ด๋ฌํ ์์ ์ ์ํด executeUpdate() ๋ผ๋ ๋ฉ์๋๋ฅผ ์ ๊ณตํด์คฌ์๋ค.
- ํด๋น ๋ฉ์๋๋ ์์์ฑ ์ปจํ
์คํธ๋ฅผ ๋ฌด์ํ๊ณ , ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ง์ ์ฟผ๋ฆฌ๋ฅผ ๋ ๋ฆฐ๋ค.
= ๋ฒํฌ์ฐ์ฐ์ ๋ณ๊ฒฝ๊ฐ์ง๊ฐ ์๋ํ์ง ์๋๋ค. ์ฌ์ฉํ๋ฉด ์์์ฑ ์ปจํ ์คํธ์ Database์ ์ ํฉ์ฑ์ด ๊นจ์ง๋ค. - ๊ทธ๋์ ๋ฒํฌ์ฐ์ฐ ์ดํ์ ์์์ฑ ์ปจํ
์คํธ๋ฅผ ์ด๊ธฐํ(em.clear)ํ๊ณ ๋ค๋ฅธ ์์
์ ํ๋ ๊ฒ์ ๊ถ์ฅํ๋ค.
๊ฐ๋ฅํ๋ฉด ๋ฒํฌ์ฐ์ฐ์ ๋จผ์ ํ๊ณ em.clear()๋ฅผ ํ๋๋ก ํ์.
์์์ฑ ์ปจํ ์คํธ๋ฅผ ์์ ํ ๋น์๋ฒ๋ฆฌ๋ฉด ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ๋ค์ ์กฐํํ๊ธฐ ๋๋ฌธ์ ์ ํฉ์ฑ์ด ๊นจ์ง์ง์๋๋ค.
์ฐธ๊ณ ๋ก JPA ๋ Update, Delete ์ฟผ๋ฆฌ๋ง ์ง์ํ๊ณ , ํ์ด๋ฒ๋ค์ดํธ์์๋ select (insert into select๋ฌธ)๊น์ง ์ถ๊ฐ ์ง์ํ๋ค.
// Update ๋ฒํฌ์ฐ์ฐ
String query = "update Product p "+
"set p.price = p.price * 1.1 " +
"where p.stockAmount < :stockAmount";
int resultCount = em.createQuery(query)
.setParameter("stockAmount", 10)
.executeUpdate();
// Delete ๋ฒํฌ์ฐ์ฐ. (Select ๋ฒํฌ์ฐ์ฐ์ JPA ์คํ์๋ ์๊ณ , ํ์ด๋ฒ๋ค์ดํธ๋ง ์ง์ํ๋ค.)
int deletedCount = em.createQuery("DELETE FROM Country").executeUpdate();
์ฐธ๊ณ ๋ก ํ์ด๋ฒ๋ค์ดํธ๋ง ์ง์ํ๋ ๋ฒํฌ์ฑ insert into select๋ ์๋์ ๊ฐ์ ๋ฐฐ์น ์ฟผ๋ฆฌ๋ฅผ ๋งํ๋ค.
// Batch
INSERT INTO table1 (col1, col2) VALUES
(val11, val12),
(val21, val22),
(val31, val32);
SpringDataJpa์์๋ ๋ฒํฌ์ฑ ์ฟผ๋ฆฌ๋ฅผ ์๋์ ๊ฐ์ด ์ ์ํ ์ ์๋ค.
public interface JpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {
// @Modifying ์ด ๋ถ์ด์์ผ๋ฉด executeUpdate()๋ฅผ ์คํํ๋ค.
// ์์ผ๋ฉด getResultList(), getSingleResult() ํธ์ถ => ์ฟผ๋ฆฌ ์์ธ(not supported DML) ๋ฐ์.
@Modifying
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);
}
์ฐธ๊ณ ๋ก ๋ฒํฌ์ฑ ์์ , ์ญ์ ์ฟผ๋ฆฌ์ @Modifying์ ๋ถ์ด์ง์์ผ๋ฉด ์๋์ ๊ฐ์ ์์ธ๊ฐ ๋ฐ์ํ๋ค.
org.hibernate.hql.internal.QueryExecutionRequestException: Not supported for DML operations
๋ฒํฌ์ฐ์ฐ์ ์์์ฑ ์ปจํ ์คํธ๋ฅผ ๋ฌด์ํ๊ณ ์คํํ๊ธฐ ๋๋ฌธ์, Database์ ์ปจํ ์คํธ๊ฐ์ ์ ํฉ์ฑ์ด ๊นจ์ง ์ ์๋ค.
- ๊ทธ๋์ ๋ฒํฌ ์ฐ์ฐ ์ดํ์๋ entityManager.clear()๋ฅผ ์คํํด์ ์ปจํ ์คํธ๋ฅผ ๋น์๋ฒ๋ฆฌ๊ณ , DB์์ ์กฐํํ๋ผ๊ณ ํ๋ค.
์ด๋ @Modifying(clearAutomatically = true)๋ก ์ค์ ํ ์ ์๋ค. ์ค์ํ ๊ฑด ์ด ์์ฑ์ ๊ธฐ๋ณธ๊ฐ์ false์ด๋ค.
flushAutomatically ๋ผ๋ ์ต์ ๋ ์๋๋ฐ, ๊ฐ๋ฅํ๋ฉด ์ด๊ฑธ ์ฐ๊ธฐ๋ณด๋ค๋ ํ ํธ๋์ญ์ ์์ ๋ฒํฌ ์ฐ์ฐ์ ์ ์ผ ๋จผ์ ํ๋๊ฒ ์ข๋ค.
@Modifying(clearAutomatically = true) // ํด๋น ์ฟผ๋ฆฌ๋ฌธ ์คํ ์ดํ, entityManager.clear() ์คํ
@Modifiying(flushAutomatically = true) // ํด๋น ์ฟผ๋ฆฌ๋ฌธ ์คํ ์ด์ , entityManager.flush() ์คํ
์ฌ์ค ์ด ๋ถ๋ถ์ ๋ฒํฌ์ฐ์ฐ ๋ฟ ์๋๋ผ, JDBC ๋ MyBatis๊ฐ์ ์ ์ฟผ๋ฆฌ๋ฅผ JPA์ ํจ๊ป ์ธ๋๋ ๋ง์ฐฌ๊ฐ์ง์ด๋ค.
๊ฐ์ ์ด์ ๋ก ์ฉ ์ฟผ๋ฆฌ๋ฅผ ๋ ๋ฆฌ๊ธฐ์ flush(๊ฐ๋ฅํ๋ฉด ์ฉ ์ฟผ๋ฆฌ๋ฅผ ์ฒ์์ผ๋ก), ๋ ๋ฆฌ๊ณ ๋์ clear๋ฅผ ํด์ฃผ๋๊ฒ ์ข๋ค.
๐ญ@EntityGraph
์์ JPA์์๋ ์ฐ๊ด๊ด๊ณ์ธ ๊ฐ์ฒด๋ฅผ Lazyํ๊ฒ ๊ฐ์ ธ์ฌ ์ ์๋ค. ์ด ๋ 1+N ๋ฌธ์ ๋ฅผ ํผํ๊ธฐ ์ํด join fetch(ํจ์น์กฐ์ธ)์ ์ ์ ํ๊ฒ ์ฌ์ฉํด์ผ ํ๋ค.
์ฐ๊ด๊ด๊ณ์ธ ๊ฐ์ฒด๋ฅผ Lazy๋ก ์ค์ ํด๋์ผ๋ฉด, ์ฌ์ฉํ๋ ์์ ๊น์ง ์ค์ ์ฟผ๋ฆฌ๋ฌธ์ด ๋ฐ์ํ์ง ์๋๋ค.
null ์ญํ ์ ํ๋ ํ๋ก์๊ฐ ์ฝ์
๋์ด ์๋ค๊ฐ, ํ๋ก์๋ฅผ ์ฌ์ฉํ๋ ์์ ์ ์๋ก์ด ์ฟผ๋ฆฌ๊ฐ ๋ฐ์ํ๋ค.
์ด๊ฒ OneToMany ๊ด๊ณ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ ์๋ค. 1๊ฐ๋ฅผ ์กฐํํ๋๋ฐ ๊ฑฐ๊ธฐ์ ์ฐ๊ด๋ 100๊ฐ์ ๊ฐ์ฒด๊ฐ ์กฐํ๋๋๋ฐ, JPA ํน์ฑ์ ํด๋น ์ฟผ๋ฆฌ๊ฐ 100๋ฒ ๋๊ฐ๊ธฐ ๋๋ฌธ.
๋ง์ฝ member.team.order ๊ฐ์ด ์ฐ๊ด๊ด๊ณ์ ๋ ๋ค๋ฅธ ์ฐ๊ด๊ด๊ณ๊ฐ ์๋ค๋ฉด? ๊ณฑํด์ ธ์ ๋ช๋ง๊ฐ์ ์ฟผ๋ฆฌ๊ฐ ๋ฐ์ํ ์ง๋ ๋ชจ๋ฅธ๋ค. ์ด๋ฅผ 1+N ๋ฌธ์ ๋ผ๊ณ ๋ถ๋ฅด๊ณ , ์ด๋ฅผ ๋ฐฉ์งํ๊ธฐ ์ํด์ JPA์์๋ join fetch, ํจ์น์กฐ์ธ์ด๋ผ๋ ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ค.
- select ... from ... join fetch (๊ฐ์ ธ์ฌ ์ฐ๊ด๊ด๊ณ ํ๋)
- select m from Member m join fetch m.team
String jpql = "select m from Member m join fetch m.team";
List<Member> members = em.createQuery(jpql, Member.class).getResultList();
// ํจ์น ์กฐ์ธ์ผ๋ก ํ์์ ๊ฐ์ ธ์ฌ๋ (์ฐ๊ด๋ ํ ๊ฐ์ฒด๋ฅผ inner join์ผ๋ก) ๊ฐ์ ธ์์ ์ง์ฐ ๋ก๋ฉ X
for (Member member : members) {
log.info("username = " + member.getUsername() + ", " +
"teamName = " + member.getTeam().name());
}
@Query("select m from Member m left join fetch m.team")
List<Member> findMemberFetchJoin();
์ฐธ๊ณ ๋ก join fetch๊ฐ ์๋๋ผ ๊ทธ๋ฅ join๋ง ์ฌ์ฉํด์๋ 1+N๋ฌธ์ ๋ฅผ ํด๊ฒฐํ์ง ๋ชปํ๋ค.
join ๋ง ์ฌ์ฉํ๋ฉด ํ ์ด๋ธ์ ์กฐ์ธํ ๋๋ง ์ฌ์ฉํ๋ค. ์ฆ DB ์กฐ์ธ ์ฟผ๋ฆฌ์๋ง ์ฌ์ฉ๋ ๋ฟ, ์์์ฑ ์ปจํ ์คํธ์ ์ฐ๊ด๋ ๊ฐ์ฒด(Team)์ ๊ฐ์ ธ์ค์ง๋ ์๋๋ค. ๊ฒฐ๊ตญ ์ฌ์ฉํ๋ ์์ ์๋ ์ถ๊ฐ์ ์ธ ์ฟผ๋ฆฌ๊ฐ ํ์ํ๋ค.
์ฌ์ค์ join fetch (ํจ์น์กฐ์ธ)์ ๊ฐํธ๋ฒ์ ์ด๋ผ๊ณ ์๊ฐํ๋ฉด ๋๋ค.
@EntityGraph๋ ์ฌ์ค์ join fetch (ํจ์น์กฐ์ธ)์ ๊ฐํธ๋ฒ์ ์ด๋ผ๊ณ ์๊ฐํ๋ฉด ๋๋ค.
JPQL ์ฟผ๋ฆฌ๋ฅผ ์ฌ์ฉํ์ง๋ ์์ง๋ง, ๋ด๋ถ ๊ตฌํ ์ฝ๋์ ๋์์ ํจ์น์กฐ์ธ๊ณผ ๊ฐ๋ค.
ํ๋ ๋ค๋ฅธ์ ์ LEFT OUTER JOIN (์๋ ์ฝ๋์์ Member๋ฅผ ๊ธฐ์ค์ผ๋ก LEFT ์กฐ์ธ)์ ์ฌ์ฉํ๋ค.
//๊ณตํต ๋ฉ์๋ ์ค๋ฒ๋ผ์ด๋
@Override
@EntityGraph(attributePaths = {"team"}) // left join fetch m.team
List<Member> findAll();
//JPQL + ์ํฐํฐ ๊ทธ๋ํ
@EntityGraph(attributePaths = {"team"}) // left join fetch m.team
@Query("select m from Member m")
List<Member> findMemberEntityGraph();
//๋ฉ์๋ ์ด๋ฆ์ผ๋ก ์ฟผ๋ฆฌ์์ ํนํ ํธ๋ฆฌํ๋ค.
@EntityGraph(attributePaths = {"team"}) // left join fetch m.team
List<Member> findByUsername(String username)
@Query("select m from Member m left join fetch m.team") // ํด๋น ๊ธฐ๋ฅ๊ณผ ๋์ผํ๋ค.
List<Member> findMemberFetchJoin();
์ ์ฌ์ฉํ์ง๋ ์์ง๋ง, @NameQuery ์ฒ๋ผ @NamedEntityGraph๋ผ๋ ๊ธฐ๋ฅ๋ ์ ๊ณตํด์ฃผ๊ธด ํ๋ค.
@Entity
@NameEntityGraph(name="Member.all", attributeNodes = @NamedAttributeNode("team"))
public class Member{
...
}
@EntityGraph("Member.all")
List<Member> findByUsername(@Param("username") String username);
๐ญ JPA Hint & Lock
@QueryHints๋ JPA ๊ตฌํ์ฒด์ ์ ๊ณตํ๋ ํํธ์ด๋ค. ๋ง์น DB์ SQL ํํธ๋ฅผ ์ ๊ณตํด์ ํ๋ํ๋ ๊ฒ์ฒ๋ผ.
@QueryHints( value = @QueryHint(name = "org.hibernate.readOnly", value ="true") )
Member findReadOnlyByUsername(String username);
๋์์ ๊ฐ์ ํ์ง๋ ์๊ณ , ์ต์ ํ์ ๋์์ ์ค๋ค.
์๋ฅผ ๋ค์ด JPA๋ ๋ณ๊ฒฝ๊ฐ์ง๋ฅผ ์ด์ฉํ์ฌ Update ์ฟผ๋ฆฌ๋ฌธ์ ๋ฐ์์ํจ๋ค. ์ฆ ๋ณ๊ฒฝํ๋ ๋ง๋ ์ต์ด ์กฐํ์์ Entity๋ฅผ ๊ฐ์ง๊ณ ์๋ค๊ฐ, ๋์ค์ ๋ณ๊ฒฝ๋์๋์ง ํ์ธ์ฉ์ผ๋ก ์ฌ์ฉํ๋ค.
๋ง์ฝ ์์ ๊ฐ์ด ํด๋น ์กฐํ ๊ฑด์ด `readOnly`์์ ์๋ ค์ค๋ค๋ฉด ๋ณ๊ฒฝ๊ฐ์ง๋ฅผ ๋ฐ๋ก ํ์ง ์๋๋ค.
๋ค๋ง ๋๋ถ๋ถ JPA ์ฟผ๋ฆฌ ์ต์ ํ๋ ๋ณ ํจ๊ณผ๊ฐ ์์ด์ ๊ฑฐ์ ์ฌ์ฉํ์ง ์๋๋ค. ๋๋ถ๋ถ ๋ง๋ค์ด์ง SQL์ ์ฟผ๋ฆฌ๋ฌธ์ ๋ฌธ์ ์ด๋ค.
๋ฐ์ดํฐ๋ฒ ์ด์ค์๋ ๋น๊ด์ Lock, ๋๊ด์ Lock ์ ์ง์ ํ๋ ๊ธฐ๋ฅ์ด ์๋๋ฐ, JPA๋ ๋ง์ฐฌ๊ฐ์ง๋ก ์ ๊ณตํ๋ค.
์ด๋ @Lock(...)์ผ๋ก ์ง์ ํ ์ ์๋ค.
@Lock(LockModeType.PESSIMISTIC_WRITE) // ๋๋ LockModeType.OPTIMISTIC_~~~
List<Member> findByUsername(String name);
๋น๊ด์ , ๋๊ด์ lock์ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ๊น๊ฒ ๊ณต๋ถํ๋ค๋ณด๋ฉด ์ ์์๋ ๋ด์ฉ์ด๋ค. ๊ฐ๋จํ๊ฒ ์ค๋ช ํ์๋ฉด
- ๋น๊ด์ (perssimistic lock) : ํธ๋์ญ์
์ถฉ๋์ด ๋๋ค ๊ฐ์ ํ๋ค. ์ฌ์ฉ์ ๋ฝ์ ๊ฑธ๊ณ , ์์ ํ๋ ์์ ์ ์ถฉ๋์ ์ ์ ์๋ค.
DB๊ฐ ์ ๊ณตํ๋ Lock ๊ธฐ๋ฅ์ ์ฌ์ฉํ๋ค. ์๋ฅผ ๋ค์ด Select for update ๋ฑ์ด ์๋ค. - ๋๊ด์ (optimisitc lock) : ํธ๋์ญ์
์ถฉ๋์ด ๋์ง ์๋๋ค๊ณ ๊ฐ์ ํ๋ค. ๋ฝ์ ๊ฑธ์ง์๊ณ ์ฌ์ฉํ๋ค.
์ฝ๊ฒ ๋งํด Lock์ ๊ฑธ์ง ์๋๋ค. ์ดํ๋ฆฌ์ผ์ด์ ๋จ์์ ์ฒ๋ฆฌํ๊ฑฐ๋ version ๊ฐ์ ๊ฐ์ ์ด์ฉํด์ ์ถฉ๋์ ๊ฐ์งํ๋ค. ์ปค๋ฐ์์ ์ ์ถฉ๋์ด ๊ฐ์ง(=version์ด ๋ค๋ฆ)๋๋ค๋ฉด ๋กค๋ฐฑํ๋ ๋ฐฉ์์ด๋ค.
์ฑ๋ฅ์ ๋น์ฐํ ํธ๋์ญ์ ์ ๋ฝ์ ๊ฑธ์ง์๋ ๋๊ด์ ๋ฝ์ด ์ข์ง๋ง, ์ถฉ๋์ด ๋ฐ์ํ์ ๋ ์ดํ๋ฆฌ์ผ์ด์ ๋จ์์ ๋ณต๊ตฌ๋ฅผ ์ํ ์ถ๊ฐ์ ์ธ ์์ ์ ํด์ค์ผํ๋ค. (๋ณด์ ํธ๋์ญ์ ๋ฑ)
๋ฝ์ DBMS๋ง๋ค ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ์ด ์กฐ๊ธ์ฉ ๋ค๋ฅด๋ค. ๋๊ด์ ๋ฝ์ ๊ฒฝ์ฐ ์ดํ๋ฆฌ์ผ์ด์ ๋จ์์๋ง ์ฒ๋ฆฌํ ์๋ ์๋ค.
์ด๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ๊ณต๋ถํ๋ค๋ณด๋ฉด ์์ฐ์ค๋ฝ๊ฒ ์๊ฒ๋๋ ๋ด์ฉ์ด๋, ์ง๊ธ์ ๊ฐ๋จํ๊ฒ๋ง ์๊ณ ๋์ด๊ฐ๋๋ก ํ์.
๐ญ ์ฌ์ฉ์ ์ ์ Repository ๊ตฌํ
SpringDataJpa์ ๊ธฐ๋ฅ์ ์ฌ์ฉํ๋ฉด ๋๋ฌด ํธํ์ง๋ง, ํน์ ๊ตฌํ๋ถ๋ถ์ ๋ณ๊ฒฝํ๊ณ ์ถ์ ์ ์๋ค. (์กฐํ ์ฟผ๋ฆฌ๋ฑ)
- ์คํ๋ง JDBC Template, MyBatis๋ฑ์ ์ฌ์ฉํ ๋
- QueryDSL๋ฑ์ ์ฌ์ฉํ ๋
์ด ๋ JpaRepository<Entity, ID> ์ ๊ตฌํ์ฒด๋ฅผ ์ง์ ๋ง๋ค์ด์ค์ผํ๋๋ฐ, ๋ฉ์๋๊ฐ ์๋ ๋ง์์ ์ฌ์ค์ ๋ถ๊ฐ๋ฅํ๋ค.
์ด๋ฐ ๊ฒฝ์ฐ์๋ [ ์ธํฐํ์ด์ค๋ช + Impl ] ์ด๋ฆ์ ๊ฐ์ง ๊ตฌํ์ฒด๋ฅผ ๋ง๋ค๋ฉด ์ํ๋ ๋ถ๋ถ๋ง ์ปค์คํ ํ์ฌ ์ฌ์ฉํ ์ ์๋ค.
// 1. ์ฌ์ฉํ๋ ๋ฉ์๋์ ์ธํฐํ์ด์ค๋ง ์ ์ํ๋ค.
public interface MemberRepositoryCustom {
List<Member> findMemberCustom();
}
//2. ํด๋น ์ธํฐํ์ด์ค๋ฅผ ๊ตฌํํ๋ค. ์ํ๋ ๋ฐฉ์์ผ๋ก ์ปค์คํ
ํ๋ค.
public class MemberRepositoryCustomImpl implements MemberRepositoryCustom {
private final EntityManager em;
public MemberRepositoryImpl(EntityManager em) { this.em = em; }
@Override
public List<Member> findMemberCustom() {
return em.createQuery("select m from Member m")
.getResultList();
}
}
//3. ๊ทธ ์ธ ๊ธฐ๋ณธ๊ฐ์ผ๋ก ์ฌ์ฉํ๊ณ ์ถ์ ๋ฉ์๋๋ ์๋์ ๊ฐ์ด ์ฃผ์
๋ฐ๋๋ค.
public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {
}
์ฐธ๊ณ ๋ก Spring Data 2.0 ์ด์ ์๋ [ JpaRepository๊ฐ ์ฐ๊ฒฐ๋ ์ธํฐํ์ด์ค์ ์ด๋ฆ ] ์ ์ฌ์ฉํ์ด์ผํ๋ค.
์ฆ MemberRepositoryImpl ๋ก ์ด๋ฆ์ ์ง์์ด์ผ ํ๋๋ฐ ์ต์ ๋ฒ์ ์์๋ ์ ์์ ์ฒ๋ผ ์ฌ์ฉํ ์ ์๋ค. ๋ ์์ฐ์ค๋ฝ๋ค.
[ ์ธํฐํ์ด์ค๋ช + Impl ] ์ด๋ฆ ๊ท์น์ ๊ฐ์ง๊ณ ์๋ค๋ฉด SpringDataJpa์์ ํด๋น ํด๋์ค๋ฅผ ์คํ๋ง ๋น์ผ๋ก ๋ฑ๋กํ๋ค.
๊ทธ๋ฆฌ๊ณ JpaRepository ๊ตฌํ์ฒด์ ์ปค์คํ ํ ๋ฉ์๋๊ฐ ์ ์ฉ๋๋๋ก ๋ง๋ค์ด์ค๋ค.
// ์ด๋ ๊ฒ ์ฌ์ฉํ๋ฉด, ๋ด๊ฐ ์ปค์คํ
ํ ๋ฉ์๋๊ฐ ์คํ๋๋ค.
List<Member> result = memberRepository.findMemberCustom();
์ฐธ๊ณ ๋ก ์ด๋ฆ ๊ท์น์ ์๋์ ๊ฐ์ด ๋ณ๊ฒฝํ ์ ์๋ค. ๋ฌผ๋ก ๊ตณ์ด ๋ณ๊ฒฝ ํด์ผํ ์ด์ ๋ ์๋ค. ๊ด๋ก๋ฅผ ๋ฐ๋ฅด์.
@EnableJpaRepositories(basePackages = "study.datajpa.repository",
repositoryImplementationPostfix = "Impl") // ์ฌ๊ธฐ
๋ค๋ง ์ด๋ ๊ฒํ๋ฉด MemberRepository๊ฐ ๋๋ฌด ๋ณต์กํด์ง๋ค๋ ๋ฌธ์ ๊ฐ ์๋ค.
๊ทธ๋์ ์์ ์กฐํ์ฑ ์ฟผ๋ฆฌ์ ์ปค๋งจ๋์ฑ ์ฟผ๋ฆฌ๋ฅผ ๋ถ๋ฆฌํด์ ์ฌ์ฉํ๊ธฐ๋ ํ๋ค. ์ด๋๋ ๊ตณ์ด CustomRepository.. ์ด๋ฐ๊ฑธ ๋ง๋ค์ง ๋ง๊ณ , ๋ฆฌํฌ์งํ ๋ฆฌ๋ฅผ ํ๋ ๋ ๋ง๋ค๋ฉด ๋๋ค.
@Repository
public class MemberQueryRepository{
private final EntityManager em;
List<Member> findAllMembers(){
...
}
}
์ด ๊ฒฝ์ฐ์๋ SpringDataJpa์๋ ๋ณ ๊ด๋ จ์๊ณ , ๊ทธ๋ฅ ์คํ๋ง ๋น์ผ๋ก ๋ฆฌํฌ์งํ ๋ฆฌ๋ฅผ ํ๋ ๋ ๋ฑ๋กํด์ ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ์ด๋ค.
๐ญ Auditing (๊ฐ์ฌ, ๋ฑ๋ก์๊ฐ, ์์ ์๊ฐ)
๋๋ถ๋ถ์ ํ ์ด๋ธ์ ๋ฑ๋ก์ผ, ์์ ์ผ์ ํ์๋ก ๋ค์ด๊ฐ๋ค. ์ด์์ํฉ์์๋ ๋ฐ์ดํฐ๋ฅผ ๊ฒ์ํ๊ฑฐ๋ ๋ง์ถฐ์ผํ๋ ์ผ์ด ๋ง์๋ฐ, ์ด ๋ ์ด๋ฐ ๊ฐ์ด ์์ผ๋ฉด ๋ก๊ทธ๋ฅผ ๋ค ๋ค์ง๋ ๋ ธ๊ฐ๋ค๋ฅผ ํ ์๋ ์๋ค.
๊ฑฐ๊ธฐ์ ํ์์ ๋ฐ๋ผ ๋ฑ๋ก์, ์์ ์๊ฐ ๋๊ตฐ์ง๋ ๊ธฐ๋กํ๊ธฐ๋ ํ๋ค. ์ด๋ฐ ๋ถ๋ถ๋ค์ Auditing์ด๋ผ๊ณ ๋ถ๋ฅธ๋ค.
์์ JPA์์ Auditing์ ํ๋ ๋ฐฉ๋ฒ
- @PrePersist / @PostPersist ๋ฉ์๋
- @PreUpdate / @PostUpdate ๋ฉ์๋
@Entity๋ง๋ค ์ง์ ์ ์ด๋ ๋์ง๋ง, ์๋์ ๊ฐ์ด ๊ณตํต๋ ๋ถ๋ถ์ @MappedSuperclass๋ก ๋ฝ์ ์ฐ๊ธฐ๋ ํ๋ค.
์ฐธ๊ณ ๋ก @MappedSuperclass๋ ์ค์ ์์ ๊ด๊ณ๊ฐ ์๋๊ณ , ์๋ฐ์ extend๋ฅผ ์ด์ฉํด ์์ฑ๋ง ๋ฐ์ ์ฐ๋ ๋ฐฉ๋ฒ์ด๋ค.
public class Member extends JpaBaseEntity {}
์๋์ ๊ฐ์ด ์ํฐํฐ๊ฐ ์์ํ ๋๊ธฐ์ / ์
๋ฐ์ดํธ ๋๊ธฐ ์ ์ ํ ํ๋์ ์ ์ํ ์ ์๋ค.
์ด๋ฅผ ์ด์ฉํด ์์ ์ผ, ๋ฑ๋ก์ผ์ ์ ์ฅํ ์ ์๋ค.
@MappedSuperclass
@Getter
public class JpaBaseEntity {
@Column(updatable = false)
private LocalDateTime createdDate;
private LocalDateTime updatedDate;
@PrePersist
public void prePersist() {
LocalDateTime now = LocalDateTime.now();
createdDate = now;
updatedDate = now;
}
@PreUpdate
public void preUpdate() {
updatedDate = LocalDateTime.now();
}
}
์คํ๋ง ๋ฐ์ดํฐ JPA์์๋ ์ด๋ฅผ ์ํด ๋ช๊ฐ์ง ์ด๋ ธํ ์ด์ ์ ์ ๊ณตํด์ค๋ค.
๋จ ์ฌ์ฉํ๊ธฐ ์ @EnableJpaAuditing์ ํ์ฑํ ํด์ฃผ์ด์ผํ๋ค.
@EnableJpaAuditing // JPA Auditing ์ ๋
ธํ
์ด์
์ ํ์ฑํ์ํต๋๋ค.
@SpringBootApplication
public class DataJpaApplication {
public static void main(String[] args) {
SpringApplication.run(DataJpaApplication.class, args);
}
}
๊ทธ๋ฆฌ๊ณ ์๋์ ์ด๋ ธํ ์ด์ ์ ์ฌ์ฉํ๋ฉด ๋๋ค.
- @EntityListeners(AuditingEntityListener.class)
- @CreatedDate / @LastModifiedDate
- @CreatedBy / @LastModifiedBy
public class Member extends BaseEntity {}
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
@Getter
public class BaseEntity {
@CreatedDate
@Column(updatable = false) // ์ด ์ค์ ์ ์ํด๋ ๋์ง๋ง, ์ผ์ข
์ ์์ ์ฅ์น.
private LocalDateTime createdDate;
@LastModifiedDate
private LocalDateTime lastModifiedDate;
}
์ฐธ๊ณ ๋ก ์ต์ด ์์ฑ ์์ ์ ์์ ์ผ์๋ ํจ๊ป ๊ธฐ๋ก๋๋ค. ๋ณดํต ์ด๋ ๊ฒ ํ๋๊ฒ ๋ณ๊ฒฝ์ ์ถ์ ํ๊ธฐ ํธํด์ ์ผ๋ฐ์ ์ด๋ค.
๋ง์ฝ ๋ฐ๊พธ๊ณ ์ถ๋ค๋ฉด @EnableJpaAuditing(modifyOnCreate = false) ๋ก ์์ ์๋ null๋ก ์์ํ๊ฒ๋ ๋ง๋ค ์๋ ์๋ค.
๋ค๋ง ์๊ฐ์ ๊ทธ๋ ๋ค ์น๋๋ผ๋, ์๋์ ๊ฐ์ด ๋ฑ๋ก์, ์์ ์๋ ์ด๋ป๊ฒ ์๋์ผ๋ก ์ถ๊ฐํ ๊น?
@LastModifiedBy
private String lastModifiedBy; // JPA๊ฐ ์์ ์ ์ด๋ฆ์ด ๋ฌด์์ธ์ง ์ด๋ป๊ฒ ์์ง?
์ด๋ ์ฐ๋ฆฌ๊ฐ ์ง์ ๋ฑ๋กํด์ฃผ์ด์ฌํ๋ค. auditorProvider๋ฅผ ๋น์ผ๋ก ๋ฑ๋กํด์ฃผ๋ฉด ๋๋ค. ๊ทผ๋ฐ ์ ์ฌ์ฉ์ ์ํ ๋ฏ.
@Bean
public AuditorAware<String> auditorProvider() {
// ๋ณดํต ์ธ์
์ ๋ณด๋, ์คํ๋ง ์ํ๋ฆฌํฐ์์ ID๋ฅผ ๋ฐ์์จ๋ค.
return () -> Optional.of(UUID.randomUUID().toString());
}
๐ ์ข ๋ ์ค์ฉ์ ์ธ ๋ฐฉ๋ฒ(@CreationTimestamp)
๊ทผ๋ฐ @EntityListeners๋ฅผ ๋ฑ๋กํ๋ ๊ณผ์ ์ด ๋ฒ๊ฑฐ๋กญ๊ธดํ๋ค. ๋ฌผ๋ก ์ ์ญ์ผ๋ก ์ค์ ํ๋ ๋ฐฉ๋ฒ๋ ์์ง๋ง ๊ทธ๋๋ ๊ท์ฐฎ๋ค.
META-INF/orm.xml ์ค์ ์ ํตํด @EntiyListeners๋ฅผ ์ํฐํฐ ์ ์ฒด์ ์๋ ์ ์ฉ์ํค๋ ๋ฐฉ๋ฒ.
<?xml version=“1.0” encoding="UTF-8”?>
<entity-mappings xmlns=“http://xmlns.jcp.org/xml/ns/persistence/orm”
xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance”
xsi:schemaLocation=“http://xmlns.jcp.org/xml/ns/persistence/
orm http://xmlns.jcp.org/xml/ns/persistence/orm_2_2.xsd”
version=“2.2">
<persistence-unit-metadata>
<persistence-unit-defaults>
<entity-listeners>
<entity-listener class="org.springframework.data.jpa.domain.support.AuditingEntityListener”/>
</entity-listeners>
</persistence-unit-defaults>
</persistence-unit-metadata>
</entity-mappings>
์ด ๋๋ ์ค์ฉ์ ์ธ ๋ฐฉ๋ฒ์ผ๋ก JPA๊ฐ ์๋ Hybernate์์ ์ ๊ณตํ๋ ์๋ ์ด๋ ธํ ์ด์ ์ ์ฌ์ฉํ ์๋ ์๋ค.
- @CreateTimestamp - ์ํฐํฐ์ ๋ํด INSERT ์ฟผ๋ฆฌ๊ฐ ๋ฐ์ํ ๋ ํ์ฌ ์๊ฐ์ ํด๋น ํ๋์ ๊ฐ์ผ๋ก ์ฑ์ด๋ค.
- @UpdateTimestamp - ์ํฐํฐ์ ๋ํด UPDATE ์ฟผ๋ฆฌ๊ฐ ๋ฐ์ํ ๋ ํ์ฌ ์๊ฐ์ ํด๋น ํ๋์ ๊ฐ์ผ๋ก ์ฑ์ด๋ค.
์ค๋ช ์ ๋ณด๋ฉด ์๊ฒ ์ง๋ง, ํด๋น ์ด๋ ธํ ์ด์ ๋ค์ Spring์ด๋ JPA์ EntityListeners์๋ ์๋ฌด๋ฐ ๊ด๋ จ์ด ์๋ค.
ํ์ด๋ฒ๋ค์ดํธ ์์ฒด์์ ์ฟผ๋ฆฌ๊ฐ ๋ฐ์ํ๋ ์์ ์ ํ๋์ ๊ฐ์ ์ฑ์์ฃผ๋ ๊ธฐ๋ฅ์ด๋ค.
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
@Entity
@Getter
public class Post {
@Id
@GeneratedValue
private Long id;
@CreationTimestamp // INSERT ์ ์๋์ผ๋ก ๊ฐ์ ์ฑ์์ค
@Column(name = "created_at")
private LocalDateTime createdAt;
@Column(name = "updated_at")
@UpdateTimestamp // UPDATE ์ ์๋์ผ๋ก ๊ฐ์ ์ฑ์์ค
private LocalDateTime updatedAt;
}
์ฐธ๊ณ ๋ก LocalDateTime์ ํ์ฌ ๊ตญ๊ฐ์ ์ ์ฉ๋๋ ์๊ฐ์ด๊ณ , ์ ์ธ๊ณ ๊ธฐ์ค์ ZonedDateTime ์ ์ฌ์ฉํ๋ฉด ๋๋ค.
๋ฌผ๋ก ์ด๋ JPA ๊ณต์ ๋ช ์ธ์ ์๋ ๊ธฐ๋ฅ์ด ์๋๋ผ์, ์ฌ์ฉํ๊ธฐ ๊บผ๋ ค์ง๊ธดํ๋ค. ํ์ง๋ง ํ์ค์ ์ผ๋ก ํ์ด๋ฒ๋ค์ดํธ๋ฅผ ์ฌ์ฉํ์ง ์๋ ๊ธฐ์ ์ ์๊ณ , ๋์ค์ ๋ณ๊ฒฝํ๊ธฐ์๋ ์ด๋ ต์ง ์๊ธฐ์ ์ฌ์ฉํด๋ ํฐ ์๊ด์๋ค๊ณ ๋ณธ๋ค.
์กฐ๊ธ ๋ ๋์๊ฐ, ๋๋ฉ์ธ์ ๊ธฐ์ ์ ์ธ ์์กด์ฑ(hibernate)๊ฐ ๋ค์ด๊ฐ๋ฉด ์๋๋ค๊ณ ์๊ฐํ ์ ์๋ค.
๊ทผ๋ฐ ์๊ฐํด๋ณด๋ฉด JPA ๋ํ ์ถ์ํ๋ ๋๋ฉ์ธ์ด ์๋, ๊ธฐ์ ์ ์ธ ์์กด์ฑ์ด๋ผ ๋ถ๋ฆฌํ๋ ๊ฒ์ด ๋ง๋ค.
ํ์ง๋ง ํ์ค์ ์ผ๋ก ์ฐ๊ด๊ด๊ณ๋ฅผ ๋๋ฉ์ธ์ ๋งคํํ๊ธฐ์๋ ๊น๋ค๋กญ๊ธฐ์, ํํ์ ๋ด์ JPA ์ ๋๊น์ง๋ ๋๋ฉ์ธ์ ์ถ๊ฐํด์ ๊ฐ๋ ์ค์ฉ์ ์ธ ์ ํ์ง๋ฅผ ๊ณจ๋๋ค๋ฉด, hibernate๋ฅผ ํฌํจํ๋๊ฒ ์ ๋๋ ํ์ฉ๊ฐ๋ฅํ ์์ค์ด๋ผ๊ณ ๋ณผ ์ ์๋ค.
๐ญ Web ํ์ฅ๊ธฐ๋ฅ1 - ๋๋ฉ์ธ ์ํฐํฐ ์ปจ๋ฒํฐ
๊ฒฐ๋ก ๋ถํฐ ๋งํ๋ฉด ์ฐ์ง๋ง๋ผ. ์ธ ์ผ ์๋ ๊ธฐ๋ฅ์ด๋ค.
๋ฌผ๋ก ํธ๋์ญ์ ์ด ์๋ ๋ฒ์์์ ์ํฐํฐ๋ฅผ ์กฐํํ์ผ๋ฏ๋ก, ๋ณ๊ฒฝํด๋ ์์์ฑ ์ปจํ ์คํธ๋ DB์ ๋ฐ์๋์ง ์๋๋ค. ์์ผ๋๊น!
@RestController
@RequiredArgsConstructor
public class MemberController {
private final MemberRepository memberRepository;
@GetMapping("/members/{id}")
public String findMember(@PathVariable("id") Member member) {
// ArgumentResolver์์ JpaRepository๋ฅผ ์ด์ฉํด์ ๊บผ๋ด์จ๋ค.
return member.getUsername();
}
}
๐ญ Web ํ์ฅ๊ธฐ๋ฅ2 - ํ์ด์ง๊ณผ ์ ๋ ฌ
ArgumentResolver์์ Pageable์ ์์ฑํด์ ๋ฐ์ ์๋ ์๋ค.
์์ฒญ URI /members? page=0 & size=3 & sort=id,desc & sort=username,desc
page : ํ์ฌํ์ด์ง offset (0๋ถํฐ ์์)
size : ํ ํ์ด์ง์ ๋
ธ์ถํ ๊ฑด ์, limit
sort : ์ ๋ ฌ ์กฐ๊ฑด (๊ธฐ๋ณธ asc)
@GetMapping("/members")
public Page<Member> list(Pageable pageable) {
Page<Member> page = memberRepository.findAll(pageable);
return page;
}
์ฐธ๊ณ ๋ก URI ์ฟผ๋ฆฌ ํ๋ผ๋ฉํ๋ฅผ ์๋ตํ๋ฉด ์คํ๋ง ๋ถํธ์ ์๋ ๊ธฐ๋ณธ ์ค์ ๊ฐ์ ์ฌ์ฉํ๋ค.
spring.data.web.pageable.default-page-size=20 /# ๊ธฐ๋ณธ ํ์ด์ง ์ฌ์ด์ฆ/
spring.data.web.pageable.max-page-size=2000 /# ์ต๋ ํ์ด์ง ์ฌ์ด์ฆ/
์ฟผ๋ฆฌ ํ๋ผ๋ฉํ๋ฅผ ์๋ตํ์ ๋ ๋ฐ๋ก ๊ธฐ๋ณธ๊ฐ์ ์ง์ ํด์ค ์๋ ์๋ค. ์๋์ ๊ฐ์ด ์ฌ์ฉํ๋ค.
@GetMapping("/members_page")
public String list(@PageableDefault(size = 12, sort = "username",
direction = Sort.Direction.DESC) Pageable pageable) {
...
}
ํ๋ฒ์ ์ฌ๋ฌ๊ฐ์ง Pageable์ ๋ฐ์์ค๋ ๋ฐฉ๋ฒ๋ ์๋ค. ์ฟผ๋ฆฌ์ ์ ๋์ฌ๋ฅผ ์ถ๊ฐํ๋ฉด ๋๋ค.
/members? member_page=0 & order_page=1
{์ ๋์ด}_page, {์ ๋์ด}_size... ์ด๋ฐ ์์ผ๋ก ์ง์ ํด์ฃผ๋ฉด ๋๋ค.
@GetMapping("/members")
public String list(
@Qualifier("member") Pageable memberPageable,
@Qualifier("order") Pageable orderPageable){
...
}
์์์ ๋ฐฐ์ด Page.map, Slice.map์ ํจ๊ป ์ด์ฉํ๋ฉด ๊ฝค๋ ์ ์ฉํ๊ฒ ์ฌ์ฉํ ์ ์๋ค.
Pageable์ ํ๋ผ๋ฉํ๋ก ๋ฐ์์, EntityDTO๋ก ๋ฐํํ๋ ์ฝ๋.
@GetMapping("/members")
public Page<MemberDto> list(Pageable pageable) {
Page<Member> page = memberRepository.findAll(pageable);
Page<MemberDto> pageDto = page.map(MemberDto::new);
return pageDto;
}
@GetMapping("/members") // ํ์ค๋ก ์์ฑํ๋ฉด ์ด๋ ๊ฒ ๋๋ค.
public Page<MemberDto> list(Pageable pageable) {
return memberRepository.findAll(pageable).map(MemberDto::new);
}
๐ ํน์ Page๋ฅผ 1๋ถํฐ ์์ํ๋๋ก ์ค์ ์ ๋ฐ๊ฟ ์ ์๋์?
์คํ๋ง ๋ฐ์ดํฐ๋ Page๋ฅผ 0๋ถํฐ ์์ํ๋ค.
๋ณดํต์ ๊ทธ๋ฅ 0 ๊ทธ๋๋ก ์ฐ๊ณ ํด๋ผ์ด์ธํธ์์ ์ฒ๋ฆฌํ๊ธด ํ์ง๋ง, ์์ ์๋ฒ์์ page=1๋ก ์ ๋ฌํ๊ณ ์ถ์ ์ ์๋ค.
๊ทธ๋ด๋ Page๋ฅผ ๊ทธ๋๋ก ๋ฐํํ์ง๋ง๊ณ , MyPage<MemberDto> ์ด๋ฐ์์ผ๋ก DTO๋ฅผ ๋ง๋ค์ด์ ๋ฐํํ๋ ๊ฑธ ๊ถ์ฅํ๋ค.
@GetMapping("/members") // ํ์ค๋ก ์์ฑํ๋ฉด ์ด๋ ๊ฒ ๋๋ค.
public MyPage<MemberDto> list(Pageable pageable) {
Page<MemberDto> = memberRepository.findAll(pageable).map(MemberDto::new);
/*.. Page<MemberDto> โก MyPage<MemberDto> ๋ณํ ..*/
return MyPage<MemberDto>
}
์คํ๋ง ์์ฒด์ page๋ฅผ 1๋ถํฐ ์์ํ๋ ์ต์ ์ด ์๊ธดํ๋ค.
๊ทผ๋ฐ ์ฌ์ฉํ๋ค๊ฐ๋ ๋์ค์ ํด๋ผ์ด์ธํธ์์ ์ค์ํ๊ธฐ ์ฌ์์ ์ถ์ฒํ์ง ์๋ ๋ฐฉ๋ฒ์ด๋ค.
# resource/application.yml
spring:
data:
web:
pageable.one-indexed-parameters = true
์๋ํ๋ฉด ์ด๋ ์ปจํธ๋กค๋ฌ ์ ์ถ๋ ฅ์์๋ง page๊ฐ -1 ๋ ๋ฟ, ์ค์ ํ์ด์ง ๊ฐ์ฒด์์ ๊ฐ์ ๊ทธ๋๋ก์ด๊ธฐ ๋๋ฌธ. ์ฌ์ฉ์ด ๋ฒ๊ฑฐ๋กญ๋ค.
https://mysite/members?page=0 (page 0์ ์ ๋ฌํด๋ page 1๋ก ์ฒ๋ฆฌํด์ค๋ค.)
https://mysite/members?page=1
{
"id": 1 // ์ฒซ๋ฒ์งธ ๋ฐ์ดํฐ๊ฐ ์ ์ ๋ฌ๋์๋ค.
"content": [...],
"pageable": {
"offset": 0,
"pageSize": 10,
"pageNumber": 0 // ??? ๋ ์ 0์ด๋
}
}
https://mysite/members?page=2
{
"id": 2 // ๋๋ฒ์งธ ๋ฐ์ดํฐ๊ฐ ์ ์ ๋ฌ๋์๋ค.
"content": [...],
"pageable": {
"offset": 0,
"pageSize": 10,
"pageNumber": 1 // ????
}
}
๐งจ SpringDataJpa์์ .save(entity)๋ฅผ ์ฌ์ฉํ ๋ ์ฃผ์ํ ์ .
์ด๋ SimpleJpaRepository.save() ์ ๊ตฌํ์ฝ๋๋ฅผ ๋ณด๋ฉด ๋ฐ๋ก ์ ์ ์๋ค.
- ์๋ก์ด ์ํฐํฐ๊ฐ ์๋๋ผ๋ฉด merge๋ก ์ ์ฅํ๋ค. ( Detached๋ ๊ฒฝ์ฐ, ์์์ฑ ์ปจํ ์คํธ์ ID๋ง ๋จ์์๋ ๊ฒฝ์ฐ)
@Transactional
@Override
public <S extends T> S save(S entity) {
Assert.notNull(entity, "Entity must not be null.");
if (entityInformation.isNew(entity)) {
em.persist(entity);
return entity;
} else {
return em.merge(entity);
}
}
JPA๋ฅผ ๋ฐฐ์ ๋ค๋ฉด ์๊ฒ ์ง๋ง, merge๋ ์ฑ๋ฅ์ ์ข์ง ์๋ค. ๋ฐ์ดํฐ ๋ณ๊ฒฝ์ JPA์ ๋ณ๊ฒฝ๊ฐ์ง๋ก ํ๋๊ฒ ๋ง๋ค.
์ ์ด์ merge๋ ์ค์์ -> ์์์ผ๋ก ๋ฐ๊ฟ ๋ ์ฐ๋๊ฑฐ์ง, ๋ฐ์ดํฐ๋ฅผ ๋ณ๊ฒฝํ๊ธฐ ์ํ ๊ธฐ๋ฅ์ด ์๋๋ค.
- persist()๋ ์๋ก์ด ์ํฐํฐ๋ฅผ DB insert ์ฟผ๋ฆฌ๋ก ์ ์ฅํ๋ค.
- merge()๋ ๋จผ์ DB select๋ฅผ ๋ ๋ ค์ ์ํฐํฐ๋ฅผ ๊ฐ์ ธ์จ ํ, ๊ฑฐ๊ธฐ์ ๊ฐ์ ์ฑ์์ ์๋ก์ด update ์ฟผ๋ฆฌ๋ฅผ ๋ ๋ฆฐ๋ค.
์ ํํ๋ ์์์ฑ ์ปจํ ์คํธ์ ํด๋น id๊ฐ ์๋ค๋ฉด update ์ฟผ๋ฆฌ๋ง ์คํํ๊ณ , ์์๋๋ง DB select๋ฅผ ์๋ํ๋ค.
์๋ก์ด ์ํฐํฐ์ธ์ง ํ๋ณํ๋ entityInformation.isNew(..)๋ ์๋์ ๊ฐ์ด ๋์ํ๋ค.
- Entity.getId() == null ( ๋ง์ฝ ๊ฐ์ฒด๊ฐ ์๋๋ผ๋ฉด 0 )์ด๋ผ๋ฉด ์๋ก์ด ๊ฐ์ฒด๋ก ํ๋จ, true๋ฅผ ๋ฐํํ๋ค.
@Entity
@Getter
class Item{
@Id
@GeneratedValue
private Long id; // ์ฌ๊ธฐ์ ๊ฐ์ด ์๋ ๊ฒฝ์ฐ์๋ง, ์๋ก์ด ์ํฐํฐ๋ก ์ทจ๊ธ
}
@GeneratedValue๋ก ์์ฑ๋ ์์ด๋๋ em.persist() ๋ฉ์๋์์ ์ฃผ์ ๋๋ค. ๊ทธ๋์ merge๊ฐ ๋ฐ์ํ ์ผ์ ์๋ค.
ํ์ง๋ง @GenratedValue๋ฅผ ์ฐ์ง ์๊ณ ์ํฐํฐ ID๋ฅผ setter๋ก ์ง์ ์ค์ ํ๋๊ฒฝ์ฐ, persist๊ฐ ์๋ merge๊ฐ ๋ฐ์ํ๋ค.
๋น์ฐํ ์์์ฑ ์ปจํ ์คํธ์๋ ๋ฏธ๋ฆฌ ์ ์ฅ๋ ์ํฐํฐ๊ฐ ์์ผ๋ฏ๋ก ์ธ๋์์ด ์ฟผ๋ฆฌ๊ฐ 2๋ฒ๋๊ฐ๋ค. (select -> update)
๋ฌธ์ ๋ ์ฟผ๋ฆฌ๊ฐ 2๋ฒ ๋๊ฐ๋ ๊ฑด ์ถ์ ์ด ๊ฐ๋ฅํ์ง๋ง, ์ ์ด๋ฐ ํ์์ด ๋ฐ์ํ๋์ง ์ฐพ๊ธฐ ๊น๋ค๋กญ๋ค.
์ ์ด์ ์ ๋ง ํน์ํ ์ํฉ์ด ์๋๋ผ๋ฉด ํ์ ์ ์ค์์ ์ํฐํฐ๋ฅผ merge() ํ ์ผ์ด ์์ด์ ์ ๋ชจ๋ฅด๋ ๊ฒ๋ ์๋ค.
๐ ๊ทธ๋ผ Id๋ฅผ ์ง์ ์ง์ ํ ๋์๋ ์ด๋ป๊ฒ ํด์ผํ๋์?
์๋์ ๊ฐ์ด interface Persistable<ID>๋ฅผ ๊ตฌํํด์ isNew() ๋ฉ์๋๋ฅผ ์ค๋ฒ๋ผ์ด๋ฉ ํด์ฃผ์.
๊ทธ๋ผ ์๋ก์ด ์ํฐํฐ๋ฅผ ์ ์ฅํ ๋ merge๊ฐ ๋ฐ์ํ๋ ์ค์๋ฅผ ๋ง์ ์ ์๋ค.
import org.hibernate.annotations.CreationTimestamp;
import org.springframework.data.domain.Persistable;
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member implements Persistable<Long> {
@Id
private Long id;
@CreationTimestamp
private LocalDateTime createdDate;
void setId(Long id) {
this.id = id;
}
@Override
public boolean isNew() {
/* entityInformation.isNew() ๋ก์ง์ ์ง์ ์์ฑ */
return createdDate == null;
}
}
๐ญ ์ ์ฌ์ฉํ์ง์๋ ๊ธฐํ๊ธฐ๋ฅ๋ค
์ค๋ฌด์์ ๊ฑฐ์ ์ฌ์ฉํ ์ผ ์์ง๋ง, ์คํ๋ง ๊ณต์๋ฌธ์์๋ ์๋ ๊ธฐ๋ฅ๋ค์ด๋ค.
QureyDSL์ด ๋๋ฌด ํธ๋ฆฌํ๊ธฐ๋ ํ๊ณ , ๊ธฐ๋ฅ ์์ฒด์ ์กฐ๊ธ์ฉ ํ์(?)๊ฐ ์์ด์ ๊ทธ๋ ๋ค. ์ฌ๋ฏธ๋ก ๋ณด๊ณ ๋์ด๊ฐ์.
๐ native ์ฟผ๋ฆฌ
JPA์์๋ SQL์ ์ง์ ์ฌ์ฉํ ์ ์๋ native ์ฟผ๋ฆฌ๋ฅผ ์ ๊ณตํด์ค๋ค.
์คํ๋ง JPA์์๋ @Qurey(nativeQurey=true)๋ก ์ถ์ํํ ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ค.
๊ณผ๊ฑฐ์๋ ๊ตฌ๋ ค์ ๊ฑฐ์ ์ฌ์ฉ์ ์ํ์ง๋ง ์ต๊ทผ์๋ ๋๋ฆ ์ธ๋งํด์ก๋ค. ๋ฌผ๋ก ๊ทธ๋๋ QueryDSL ์ฐ์ง ์ด๋ฐ๊ฑฐ ์์ด๋ค.
public interface JpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {
// ์ด๋ ๊ฒ DB SQL์ ๊ทธ๋๋ก ์ฌ์ฉํ ์ ์๋ค.
@Query(value = "select * from member where username = ?", nativeQuery = true)
Member findByNativeQuery(String username);
}
์ฑ ์คํ์์ ์ ํด๋น ์ฟผ๋ฆฌ์ ์ ํจ์ฑ์ ์ ์ ์๊ณ , JPQL๊ณผ ์ฉ SQL์ ์ฐจ์ด๋ก ์ฌ์ฉํ๊ธฐ ๋งค์ฐ ๋ณต์กํ๋ค.
ํ์ง๋ง ๊ทธ๋ผ์๋ ๋ถ๊ตฌํ๊ณ '์ธ๋งํด์ก๋ค'๊ณ ๋งํ ์ด์ ๋, [์ธํฐํ์ด์ค DTO]์ ํจ๊ป ์ฌ์ฉํ๋ฉด ๋๋ฆ ์ ์ฉํ๊ธฐ ๋๋ฌธ์ด๋ค.
public interface JpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {
@Query(value = "SELECT m.member_id as id, m.username, t.name as teamName " +
"FROM member m left join team t",
countQuery = "SELECT count(*) from member", // JPA ์ฟผ๋ฆฌ๊ฐ ์๋. count ์ฟผ๋ฆฌ ํ์
nativeQuery = true)
Page<MemberProjection> findByNativeProjection(Pageable pageable);
}
ํ์ง๋ง QueryDSL์ด ์๊ธฐ ๋๋ฌธ์ ์ด๋ฐ ๊ฑฐ ์์ด๋ค.
๊ทนํ์ ์กฐํ ์ฑ๋ฅ์ด ํ์ํ๋ค๋ฉด ๊ทธ๋ฅ ์ฉ์ฟผ๋ฆฌ - JdbcTemplate ์ด๋ MyBatis, jooq ๊ฐ์ ์ธ๋ถ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ฌ์ฉ์ ๊ถ์ฅํ๋ค.
์ฌ์ค Hadoop & Spark & kafka ์กฐํฉ์ด ๋์จ ๋ค๋ก๋ ์์ฒญ๋๊ฒ ๋ณต์กํ ์ฟผ๋ฆฌ๋ฅผ ์งค ์ผ์ด ๋ง์ด ์ค์๊ธด ํ๋ค.
๐ DTO Projections (DTO ํ๋ก์ ์ )
์ด๊ฑด ์ ๊ธฐ๋ฅ์ด๋ผ๊ธฐ๋ณด๋จ, Repository์์ Entity๊ฐ ์๋ ์ปค์คํ ๊ฐ์ฒด(EntityDTO)๋ฅผ ๋ฐํ๋ฐ๋ ๊ฑธ ์๋ฏธํ๋ค.
๊ฒฐ๋ก ๋ถํฐ ๋งํ๋ฉด, ๋๋ฆ ์ ์ฉํ ๊ธฐ๋ฅ์ด๋ค.
๋ค๋ง ๋จ์ ์ด DTO ํ๋ก์ ์ ๋์์ด root ์ํฐํฐ๋ฉด ์๊ด์๋๋ฐ, ๊ทธ๊ฒ ์๋๋ผ๋ฉด JPQL select ์ต์ ํ๊ฐ ์๋๋ค.
( Item - ItemOption - Option )์์ Item ์ํฐํฐ ์ ๋๋ ๊ด์ฐฎ์ง๋ง, ๊ทธ ๋ฐ์ ์๋๊ฑด ์ต์ ํ๊ฐ ์๋๋ค๋ ๋ง.
๋๋ถ๋ถ์ ๊ฒฝ์ฐ, DTO ํ๋ก์ ์ ์ ์กฐํ์ฑ๋ฅ์ ๋์ด๊ธฐ ์ํด์ ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์ ๊ทธ๋ฅ QureyDSL์ ์ฌ์ฉํ๋ ๊ฒ์ ๊ถ์ฅํ๋ค.
์ฑ๋ฅ์ด ์ค์ํ์ง ์๋ค๋ฉด ๊ตณ์ด DTO๋ก ๋ฐ๊ธฐ๋ณด๋จ, ๊ทธ๋ฅ Entity๋ฅผ ๋ฐ์์ ๋ณํํ๋๊ฒ ์ ์ง๋ณด์ ์ธก๋ฉด์์ ๋ ๋์ ์ ์๋ค.
๊ฐ๋จํ๊ฒ ์๊ฐ๋ฅผ ํด๋ณด์๋ฉด ์๋์ ๊ฐ์ด ์ธํฐํ์ด์ค๋ฅผ DTO๋ก ์ฌ์ฉํ ์๋ ์๋ค.
// 1. ์ธํฐํ์ด์ค๋ฅผ ๋ง๋ ๋ค.
public interface UsernameOnly {
String getUsername();
}
// 2. ๊ทธ๋ฅ ์ด๋ค. (ํ๋ก์ ๊ฐ์ฒด๊ฐ ์ฝ์
๋๋ค.)
public interface JpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {
List<UsernameOnly> findByUsername(String username);
}
์ด ๊ธฐ๋ฅ์ interface ๋ฟ ์๋๋ผ Class๋ ๋น์ฐํ ๋๋ค. ์ฌ์ง์ด๋ ์๋์ ๊ฐ์ด ์ ๋ค๋ฆญ๊น์ง ์ง์ํ๋ค.
์ ๋ค๋ฆญ ์ฒ๋ผ ํ์ ์ ๋ฐ๋ผ ๋ค๋ฅด๊ฒ ๊ฐ์ด ์ฑ์์ง๋ ๊ฒ์ Dynamic Projection์ด๋ผ๊ณ ํ๋ค. (๋์ ํ๋ก์ ์ )
// ์ฐธ๊ณ ๋ก find(...)By ์ด ์ฌ์ด์๋ ๋ด๊ฐ ์ํ๋ ์ด๋ฆ์ ๋ฃ์ผ๋ฉด ๋๋ค.
<T> List<T> findProjectionsByUsername(String username, Class<T> type);
List<UsernameOnly> result = memberRepository.findProjectionsByUsername("m1", UsernameOnly.class);
์ค์ฒฉ ๊ตฌ์กฐ๋ ์ง์ํ๋ค.
public interface NestedClosedProjection {
String getUsername();
TeamInfo getTeam();
interface TeamInfo {
String getName();
}
}
/*
์ฐธ๊ณ ๋ก Member, Team์ ์กฐ์ธํด์ ํ์ํ๊ฑฐ๋ง ๊ฐ์ ธ์จ๋ค.
select
m.username as col_0_0_,
t.teamid as col_1_0_,
t.teamid as teamid1_2_,
t.name as name2_2_
from
member m
left outer join
team t
on m.teamid=t.teamid
where
m.username=?
*/
SpEL ๋ฌธ๋ฒ ์ด์ฉํด์ ์๋ก์ด ๊ฐ์ ๋ง๋ค์ ์๋ค. (*๋ค๋ง DB์์ ํ๋๋ฅผ ์กฐํํ ํ์ ๊ฐ๊ณตํ๋๊ฑฐ๋ผ, ์ฟผ๋ฆฌ ์ต์ ํ๋ ์๋๋ค.)
์ฐธ๊ณ ๋ก ์ฟผ๋ฆฌ๋ DTO๋ ์ ํํ๊ฒ ์ผ์นํ๋ฉด close projection, ํ์์๋ ๋ฐ์ดํฐ๋ฅผ ๋ ๊ฐ์ ธ์ค๋ฉด open projection๋ผ๊ณ ๋ถ๋ฅธ๋ค.
public interface UsernameOnly {
@Value("#{target.username + ' ' + target.age + ' ' + target.team.name}")
String getUsername();
}
๐ Predicate ๊ธฐ๋ฅ (*DDD ์ Specifications)
๊ฒฐ๋ก ๋ถํฐ ๋งํ์๋ฉด ์์ด๋ค. JPA Criteria๋ฅผ ์ถ์ํํด์ ๋ง๋ ๊ธฐ๋ฅ์ธ๋ฐ, Criteria๋ฅผ ์๋ฌด๋ ์์จ์ ์ด๊ฒ๋ ํจ๊ป ์์ด๋ค.
DDD์์ ์๊ทธ๋ฆฌ๊ฑฐํธ๊ฐ ํน์ ์กฐ๊ฑด(์คํ)์ ๋ง์กฑํ๋์ง ๊ฒ์ฆํ ๋, Specification์ ์ถ์ํํ์ฌ ์ฌ์ฉํ๊ธฐ๋ ํ๋ค.
public interface Specification<T> {
public boolean isSatisfiedBy(T agg);
}
public class OrdererSpec implements Specification<Order> {
private String ordererId;
public OrdererSpec(String ordererId) {
this.overerId = ordererId;
}
@Override
public boolean isSatisfiedBy(Order agg) {
return agg.getOrdererId().getMemberId().getId().equals(ordererId);
}
}
์ด๋ฌ๋ฉด ์ด Specification๋ค์ ๋ ผ๋ฆฌ์ฐ์ฐ (and, or)์ผ๋ก ์กฐํฉํ๊ฑฐ๋, ์๋์ ๊ฐ์ด ๋ง์กฑํ๋ ์ํฐํฐ๋ฅผ ํํฐ๋งํ๊ธฐ ์ข๋ค.
Specification<Order> ordererSpec = new OrdererSpec("madvirus");
List<Order> orders = orderRepository.findAll(ordererSpec);
AndSpec<T> spec = new AndSpec(ordererSpec, orderDateSpec); // Spec1 And Spec2
List<Order> orders = orderRepository.findAll(spec);
public class MemoryOrderRepository implements OrderRepository {
public List<Order> findAll(Specification spec) {
List<Order> allOrders = findAll(); // ํด๋น ์คํ์ ๋ง์กฑํ๋ ์ํฐํฐ๋ง ๋ฐํ.
return allOrders.stream().filter(order -> spec.isSatisfiedBy(order)).collect(toList());
}
}
SpringDataJpa๋ JPA Criteria๋ฅผ ํ์ฉํด์ ๋ง๋ Predicate๋ผ๋ ๊ธฐ๋ฅ์ด ์๋ค.
๋ค๋ง Jpa Criteria๊ฐ ๋๋ฌด ๊ตฌ๋ ค์ ์๋ฌด๋ ์ฌ์ฉํ์ง ์๊ธฐ์ ํจ๊ป ๋ฌปํ๋ฒ๋ฆฐ ์ํ๊น์ด ๊ธฐ๋ฅ
Specification<> ๊ณผ JpaSpecificationExecutor<>๋ฑ์ ์ด์ฉํด์ ์์ ์์ ์ฒ๋ผ ์ฌ์ฉํ ์ ์๋ค.
๊ฒ๋ณด๊ธฐ์ ํธํ๊ณ ์ ๋ฐํ๊ธด ํ์ง๋ง, ์ด๋ฅผ ์ฐ๊ณ ์ถ์ผ๋ฉด JPA Criteria ์ ๋ฐฐ์์ผํ๋ค. ๋๋ฌด ๋ณต์กํ๊ณ ์ฌ์ฉํ๊ธฐ ์ด๋ ต๋ค.
// org.springframework.data.jpa.domain.Specification
Specification<Member> spec = MemberSpec.teamName("A").and( MemberSpec.username("kim") );
List<Member> result = memberRepository.findAll(spec);
๐ Qurey By Example (์ฌ์ฉํ์ง ์๋ ์ ๊ธฐ๋ฅ)
๊ฒฐ๋ก ๋ถํฐ ๋งํ์๋ฉด ์์ด๋ค. ์ฌ์ฉํ์ง ์๋ ๊ธฐ๋ฅ์ด๋ค. ๋์ ์ฟผ๋ฆฌ๋ฅผ Example๊ฐ์ฒด๋ก ์๋ ์์ฑํด์ฃผ๋ ๊ธฐ๋ฅ์ด๋ค.
๋ค๋ง ์ข์์ ์ JpaRepository์ ์ด๋ฏธ ํฌํจ๋์ด์์ด์, ๋ณ๋์ ๊ตฌ์ฑ์์ด ๋ฐ๋ก ์ฌ์ฉํ ์ ์๋ค.
interface JpaRepository๋ฅผ ๊น๋ณด๋ฉด ์์์ ์ธ๊ธํ์ง์์ QueryByExampleExecutor<T> ๋ผ๋๊ฒ ์๋ค.
์ด๋ฅผ ์ด์ฉํด์ ๋์ ์ฟผ๋ฆฌ๋ฅผ ์๋์ผ๋ก ์์ฑํด์ ์ฒ๋ฆฌํ ์ ์๋ค.
Member member = new Member("m1");
Example<Member> example = Example.of(member); // Example์ ์์ฑํ๋ค.
// Member("m1")๊ณผ ๋์ผํ ๋ชจ๋ ์ํฐํฐ๋ฅผ ์กฐํ.
List<Member> result = repository.findAll(example);
๋ณต์กํ ์กฐํ ์กฐ๊ฑด์ด ์๋ค๋ฉด, ExampleMatcher๋ฅผ ์ฌ์ฉํ๋ฉด ๋๋ค.
//ExampleMatcher ์์ฑ, age ๊ฐ์ ๋ฌด์
ExampleMatcher matcher = ExampleMatcher.matching()
.withIgnorePaths("age");
// age๋ฅผ ์ ์ธํ ๋ค๋ฅธ ํ๋ ๊ฐ์ด ๊ฐ์ Member๋ค์ ์กฐํ
Example<Member> example = Example.of(member, matcher);
List<Member> result = memberRepository.findAll(example);
๊ฐ๋จํ ์กฐ๊ฑด์ด๋ฉด ์ ์ฉํ๊ฒ ์ฐ๊ฒ ์ง๋ง, ์ค์ ์กฐํ์์๋ ์กฐ๊ฑด์ด ๋ณต์กํ๊ฑฐ๋ ํ ์ด๋ธ์ ์กฐ์ธํ ์ผ์ด ๋ง๋ค.
ํ์ง๋ง Example์ ๊ธฐ์ ์ ํ๊ณ์, Inner Join์ ์ ์ธํ ๋ค๋ฅธ ์กฐ์ธ์ ์ ๊ณตํด์ฃผ์ง ์๋๋ค.
// ์ฌ์ค ์กฐ์ธ์ด ์๋๋๋ผ๋, ์๋์ ๊ฐ์ด ๋ณต์กํ ์กฐ๊ฑด๋ ์ฒ๋ฆฌ๊ฐ ๋ถ๊ฐ๋ฅํ๋ค.
firstname = ?0 or (firstname = ?1 and lastname = ?2)
๊ตณ์ด ์ด๋ ๊ฒ ํ์ง์๊ณ ๊ทธ๋ฅ QueryDSL์ ์ฐ๋๊ฒ ํจ์ฌ ๊น๋ํ๋ค. ์ฆ ์ฌ์ฉํ์ง ์๋ ๊ธฐ๋ฅ.
๋ค๋ง QueryByExample์ ๋น๊ต์ ์ต๊ทผ(2020, 1.1๋ฒ์ )์ ๋์จ ๊ธฐ๋ฅ์ด๊ณ SpringData์ ํฌํจ๋ ๊ธฐ๋ฅ์ด๋ผ JPA์ ์์กด์ฑ์ด ์์ด์, ๋ฏธ๋์ ๋์ฌ ์คํ๋ง ์ ๋ฒ์ ์์ ๊ธฐ๋ฅ์ด ์ข์์ง๋ค๋ฉด ์ฌ์ฉํ ์๋ ์๋ค.
'๐ฑBackend > JDBC & JPA' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
์คํ๋งJPA์ ์์์ฑ์ปจํ ์คํธ (EntityManager) (0) | 2022.02.03 |
---|---|
QueryDSL + JPA (0) | 2022.02.02 |
JPA ์ฑ๋ฅ ๊ฐ์ ํ (0) | 2022.01.22 |
JPA 10 # ๊ฐ์ฒด ์ฟผ๋ฆฌ ์ธ์ด, JPQL (0) | 2021.11.16 |
JPA #9 ๊ฐ ํ์ , ์ปฌ๋ ์ , ์๋ฒ ๋๋ ํ์ (0) | 2021.11.09 |
๋ธ๋ก๊ทธ์ ์ ๋ณด
JiwonDev
JiwonDev