대부분 interface 기반 projection을 사용한다.
인터페이스 이름은 아무거나 상관없이 지어도 되고 메서드는 해당 entity와 매칭이 되는 getter를 만들어 주면 된다.
중첩 인터페이스 사용 시 여러개의 entity를 조합한 필드들을 사용할 수 있다.
벌크 insert 같은 경우는 jpa를 사용하지 않는 것이 좋다.
하나의 테이블에 컬럼을 여러개 받아오는 경우는 @JoinColumn을 여러개 사용해주면 된다.
원래는 @JoinColumns({@JoinColumn, @JoinColumn}) 형태로 사용했는데 버전이 올라가면서 @JoinColumn을 여러개 사용하는 것으로 바뀌었다.
@Configuration
@EnableWebMvc
@EnableSpringDataWebSupport
public class WebConfig {
// ...
}
Spring data가 insert를 하기 전에 select로 존재하는지 확인을 하고 insert를 한다. 이 것을 안 하기 위해서는 entity에서 Persistable을 구현하여 isNew를 재정의 하면 되는데 이 것을 사용하지 않으면 casecade를 둘 다 설정해야 한다.
이유는 SimpleJpaRepository에서 없으면 persist하고 있으면 merge를 하기 때문이다.
Dto에서 getter이름을 내가 정하고 싶을 때는 @Value(#{target.fieldName})을 추가해주면 된다.
Entity를 핸들러 메서드의 필드로 받을 수 있지만 사용하지 않는 것이 좋다.
왜냐면 Layered Architecture에서는 아래 계층이 위 계층에 역진하면 안되기 때문이다.
@RestController
@RequestMapping("/items")
public class ItemController {
@Autowired
private ItemService itemService;
@GetMapping
public List<ItemDto> getItems(Pageable pageable) { // GET /items?page=0&size=30
return itemService.getItems(pageable);
}
}
public interface Pageable {
int getPageNumber();
int getPageSize();
int getOffset();
Sort getSort();
Pageable next();
Pageable previousOrFirst();
Pageable first();
boolean hasPrevious();
}
// ?page=0&size=30
PageRequest.of(0, 30);
@Service
public class ItemServiceImpl implement ItemService {
public List<ItemDto> getItems(Pageable pageable) {
Page<Item> itemPage = itemRepository.findAll(pageable);
// ...
}
}
public interface Page<T> extends Slice<T> {
int getTotalPages();
long getTotalElements();
// ...
}
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();
// ...
}
find와 By 사이에는 아무런 문자가 와도 상관이 없다.
Slice 인터페이스를 Page 대신 사용하면 count를 사용하지 않고 페이지네이션을 지원한다.
mysql에서는 count 함수의 효율이 좋지 않기 때문에 10억개를 count 하게 되면 무리가 올 수 있다.
Slice를 사용하면 size + 1 만큼의 record를 가져오는 것을 시도해서 가져와지면 다음 페이지를 확인한다.
Lazy loading을 하게 되면 컨트롤러에서 반환을 하게 되었을 때 fetch를 하기 위해 쿼리를 날려야 하는데 이 때는 트랜젝션의 범위를 벗어났기 때문에 쿼리를 날릴 수 없다.
이 방버을 해결하기 위해 OSIV를 사용하여 트랜젝션의 범위를 늘려 해결할 수 있지만 이 방법을 추천하진 않는다.
이유는 트랜젝션은 범위가 좁을수록 lock을 잡는 시간이 짧아져서 다른 곳에서 lock을 잡으려고 할 때 기다리는 시간이 짧아지기 때문에 좋다.
OSIV를 키게 되면 트랜젝션의 범위가 Controller까지 넓어지게 되는데 범위가 커지면 애플리케이션이 느려질 수 있다.
SELECT DISTINCT post
FROM Post post
JOIN post.postUsers postUser
JOIN postUser.projectMember projectMember
JOIN projectMember.member member
WHERE member.name = 'dongmyo'
EntityManager em = ...;
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<PostEntity> cq = cb.createQuery(PostEntity.class);
Root<PostEntity> post = cq.from(Post.class);
cq.select(post);
TypedQuery<PostEntity> q = em.createQuery(cq);
List<PostEntity> posts = q.getResultList();
SELECT post FROM PostEntity post
from(entity)
.where(/* conditions */)
.list();
dsl(domain specific language) : 도메인 특정 언어는 특정 종류의 문제에 최적화된 높은 수준의 추상화를 갖춘 프로그래밍 언어이다. DSL은 필드 또는 도메인의 개념과 규칙을 사용한다.
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<version>5.0.0</version>
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
<version>5.0.0</version>
</dependency>
<build>
<plugins>
<plugin>
<groupId>com.mysema.maven</groupId>
<artifactId>apt-maven-plugin</artifactId>
<version>1.1.3</version>
<configuration>
<processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
</configuration>
<executions>
<execution>
<phase>generate-sources</phase>
<goals>
<goal>process</goal>
</goals>
<configuration>
<outputDirectory>target/generated-sources/annotations</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
Querydsl 라이브러리의 apt는 annotation processing tool인데 이 것이 어떤 역할을 하냐면 우리가 @Entity로 만들어놓은 엔티티를 Q를 붙여서 Q타입 클래스로 만들어준다.
여기서 두 번째만 사용한다.
첫 번째는 기존에 했던 JPA와 차이가 별로 없고 궁극적으로는 join이 안된다.
@Repository
public abstract class QuerydslRepositorySupport {
protected JPQLQuery from(EntityPath<?>... paths) { /* ... */ }
protected DeleteClause<JPADeleteClause> delete(EntityPath<?> path) { /* ... */ }
protected UpdateClause<JPAUpdateClause> update(EntityPath<?> path) { /* ... */ }
// ...
}
public interface JPQLQuery<T> {
JPQLQuery<T> from(EntityPath<?>... sources);
<P> JPQLQuery<T> from(CollectionExpression<?,P> target, Path<P> alias);
<P> JPQLQuery<T> innerJoin(EntityPath<P> target);
<P> JPQLQuery<T> innerJoin(EntityPath<P> target, Path<P> alias);
<P> JPQLQuery<T> join(EntityPath<P> target);
<P> JPQLQuery<T> leftJoin(EntityPath<P> target);
<P> JPQLQuery<T> rightJoin(EntityPath<P> target);
// ...
}
@NoRepositoryBean
public interface ItemRepositoryCustom {
// querydsl로 복잡한 쿼리를 수행할 메서드.
List<Item> complexQuery();
}
뒤에 Custom을 붙이는 것은 관례이다.
CustomRepository에는 @NoRepositoryBean을 사용해서 Bean으로 등록되지 않게 하여야 한다.
public class ItemRepositoryImpl extends QuerydslRepositorySupport implements ItemRepositoryCustom {
public ItemRepositoryImpl() {
super(Item.class);
}
@Override
public List<Item> complexQuery() {
QItem item = QItem.item;
JPQLQuery query = from(item).where(/* ... */);
// ...
}
}
public interface ItemRepository extends ItemRepositoryCustom, JpaRepository<Item, Long> {
}
@Autowired
ItemRepository itemRepository;
List<Item> items = itemRepository.complexQuery();
querydsl에서 Dto를 반환하게 하려면 select에서 Projections.constructor를 설정하여 파라미터로 entity 클래스와 Dto 내부 필드들을 적어주면 된다.
return from(orderItem)
.innerJoin(orderItem.order, order)
.innerJoin(orderItem.item, item)
.where(orderItem.quantity.gt(quantity))
.select(Projections.constructor(OrderDto.class, item.price, orderItem.quantity))
.distinct()
.fetch();
itemRepository.findById(1L);
select
item0_."item_id" as item_id1_4_0_,
item0_."item_name" as item_nam2_4_0_,
item0_."price" as price3_4_0_
from
"Items" item0_
where
item0_."item_id"=1
itemRepository.findAll();
select
item0_."item_id" as item_id1_4_,
item0_."item_name" as item_nam2_4_,
item0_."price" as price3_4_
from
"Items" item0_
itemRepository.findAll()
.stream()
.map(Item::getOrderItems)
.flatMap(Collection::stream)
.collect(Collectors.summarizingInt(OrderItem::getQuantity));
select
item0_."item_id" as item_id1_4_,
item0_."item_name" as item_nam2_4_,
item0_."price" as price3_4_
from
"Items" item0_
select
orderitems0_."item_id" as item_id4_8_0_,
orderitems0_."line_number" as line_num1_8_0_,
orderitems0_."order_id" as order_id2_8_0_,
orderitems0_."line_number" as line_num1_8_1_,
orderitems0_."order_id" as order_id2_8_1_,
orderitems0_."item_id" as item_id4_8_1_,
orderitems0_."quantity" as quantity3_8_1_,
order1_."order_id" as order_id1_9_2_,
order1_."order_date" as order_da2_9_2_
from
"OrderItems" orderitems0_
inner join
"Orders" order1_
on orderitems0_."order_id"=order1_."order_id"
where
orderitems0_."item_id"=1
select
orderitems0_."item_id" as item_id4_8_0_,
orderitems0_."line_number" as line_num1_8_0_,
orderitems0_."order_id" as order_id2_8_0_,
orderitems0_."line_number" as line_num1_8_1_,
orderitems0_."order_id" as order_id2_8_1_,
orderitems0_."item_id" as item_id4_8_1_,
orderitems0_."quantity" as quantity3_8_1_,
order1_."order_id" as order_id1_9_2_,
order1_."order_date" as order_da2_9_2_
from
"OrderItems" orderitems0_
inner join
"Orders" order1_
on orderitems0_."order_id"=order1_."order_id"
where
orderitems0_."item_id"=2
@Query("select i "
+ "from Item i "
+ "left outer join fetch i.orderItems oi "
+ "inner join fetch oi.order o")
List<Item> getAllItemsWithAssociations();
return from(member)
.innerJoin(member.locker, locker).fetchJoin()
.leftJoin(member.memberDetails, memberDetail).fetchJoin()
.fetch();
fetch join을 사용하면 미리 연관된 Entity를 전부 다 가져와서 영속성 컨텍스트에 넣어준다.
fetch join과 pagination을 같이 사용하면 멸망한다.
이유는 메모리상에 몸든 레코드를 다 가져오고 페이지네이션을 실행하기 때문에 OOM(OutOfMemoryError)이 발생할 수 있다.
OneToMany에서 fetch를 사용하면 순서가 없고 중복이 허용되기 때문에 오류가 발생한다.
순서를 주거나 중복을 허용하지 않으면 해결할 수 있다.
순서를 주는 방법은 추천하지 않는다.
이유는 순서라는 컬럼 하나가 더 추가되기 때문이다.
@NamedEntityGraph(name = "itemWithOrderItems", attributeNodes = {
@NamedAttributeNode("orderItems")
})
@NamedEntityGraph(name = "itemWithOrderItemsAndOrder", attributeNodes = {
@NamedAttributeNode(value = "orderItems", subgraph = "orderItems")
}, subgraphs = @NamedSubgraph(name = "orderItems", attributeNodes = {
@NamedAttributeNode("order")
}))
@Entity
public class Item {
//...
}
@EntityGraph("itemWithOrderItems")
List<Item> readAllBy();