728x90

JPA에서 Specification 인터페이스를 사용하면 동적 쿼리를 작성할 수 있습니다. 동적 쿼리란, 실행 시점에 조건을 추가하여 쿼리를 생성하는 방식입니다. Specification 인터페이스를 사용하면 쿼리를 작성할 때마다 조건을 추가할 필요 없이, 메서드 호출만으로 조건을 추가할 수 있습니다.

다음은 Specification 인터페이스를 사용한 JPA 코드입니다.

예를 들어, Customer 엔티티를 검색할 때, 이름과 이메일 주소로 검색하는 기능을 구현해 보겠습니다.

public interface CustomerSpecification {
    static Specification<Customer> hasName(String name) {
        return (root, query, builder) -> builder.like(root.get("name"), "%" + name + "%");
    }

    static Specification<Customer> hasEmail(String email) {
        return (root, query, builder) -> builder.like(root.get("email"), "%" + email + "%");
    }

    static Specification<Customer> hasNameOrEmail(String name, String email) {
        return hasName(name).or(hasEmail(email));
    }
}

위 코드에서 CustomerSpecification 인터페이스는 Specification<Customer>를 반환하는 hasName, hasEmail, hasNameOrEmail 메서드를 정의하고 있습니다. 이 메서드들은 각각 이름, 이메일, 이름 또는 이메일로 검색하는 Specification을 반환합니다.

다음은 CustomerRepository 인터페이스에서 CustomerSpecification 인터페이스를 사용하는 코드입니다.

@Repository
public interface CustomerRepository extends JpaRepository<Customer, Long>, JpaSpecificationExecutor<Customer> {
    List<Customer> findAll(Specification<Customer> specification);
}

위 코드에서 CustomerRepository 인터페이스는 JPA의 JpaRepository<Customer, Long>와 JpaSpecificationExecutor<Customer>를 상속받고 있습니다. JpaSpecificationExecutor 인터페이스는 Specification을 사용하여 동적 쿼리를 작성할 수 있도록 해줍니다.

다음은 CustomerService 클래스에서 CustomerRepository 인터페이스를 사용하여 Customer 엔티티를 검색하는 코드입니다.

@Service
public class CustomerService {
    private final CustomerRepository customerRepository;

    @Autowired
    public CustomerService(CustomerRepository customerRepository) {
        this.customerRepository = customerRepository;
    }

    public List<Customer> search(String name, String email) {
        Specification<Customer> specification = CustomerSpecification.hasNameOrEmail(name, email);
        return customerRepository.findAll(specification);
    }
}

위 코드에서 CustomerService 클래스는 CustomerRepository 인터페이스를 사용하여 Customer 엔티티를 검색하는 search 메서드를 정의하고 있습니다. 이 메서드는 CustomerSpecification 인터페이스에서 반환하는 Specification을 사용하여 동적 쿼리를 작성합니다.


내저장소 바로가기 luxury515

728x90

ispresent vs ifpresent 의 차이점

Optional<OrderDto> optionalOrderDto = queryFactory
        .select(Projections.constructor(OrderDto.class, order, user))
        .from(order)
        .leftJoin(order.user, user)
        .where(order.id.eq(orderId))
        .fetchOneOptional();

if (optionalOrderDto.isPresent()) {
    OrderDto orderDto = optionalOrderDto.get();

    List<OrderItemDto> orderItemDtos = queryFactory
        .select(Projections.constructor(OrderItemDto.class, item))
        .from(orderItem)
        .join(orderItem.item, item)
        .where(orderItem.order.id.eq(orderId))
        .fetch();

    orderDto.setOrderItemDtos(orderItemDtos);

    return orderDto;
} else {
    throw new NotFoundException("Order not found");
}

위 코드에서는 isPresent() 메서드를 사용하여 Optional 객체가 null인지 체크하고, get() 메서드를 사용하여 OrderDto 객체를 가져옵니다. OrderDto 객체가 null이 아니면 해당 객체를 사용하여 쿼리를 실행하고, null이면 NotFoundException 예외를 발생시킵니다.

반면에 ifPresent()를 사용하면 다음과 같이 null 체크와 쿼리 실행을 한 번에 할 수 있습니다.

queryFactory
        .select(Projections.constructor(OrderDto.class, order, user))
        .from(order)
        .leftJoin(order.user, user)
        .where(order.id.eq(orderId))
        .fetchOneOptional()
        .ifPresent(orderDto -> {
            List<OrderItemDto> orderItemDtos = queryFactory
                    .select(Projections.constructor(OrderItemDto.class, item))
                    .from(orderItem)
                    .join(orderItem.item, item)
                    .where(orderItem.order.id.eq(orderId))
                    .fetch();

            orderDto.setOrderItemDtos(orderItemDtos);
        })
        .orElseThrow(() -> new NotFoundException("Order not found"));

위 코드에서는 ifPresent() 메서드를 사용하여 Optional 객체가 null이 아닐 때만 람다식을 실행하고, orElseThrow() 메서드를 사용하여 NotFoundException 예외를 발생시킵니다.

isPresent()ifPresent() 모두 null 체크를 수행하지만, ifPresent()는 null 체크와 쿼리 실행을 한 번에 처리할 수 있습니다. 그러나 ifPresent()는 리턴값을 갖지 않기 때문에, 리턴값을 사용해야 하는 경우에는 isPresent()get() 메서드를 사용해야 합니다.


내저장소 바로가기 luxury515

728x90
orElseorElseGet의 차이점은 메서드가 실행되는 시점입니다.
String email = userRepository.findById(id)
        .map(User::getEmail)
        .orElse("Unknown");
위 코드에서는 orElse를 사용하여 "Unknown"을 반환하도록 설정하였습니다. 만약 orElseGet 을 사용한다면 아래와 같이 작성할 수 있습니다.
String email = userRepository.findById(id)
        .map(User::getEmail)
        .orElseGet(() -> {
            logger.warn("User email is not found. Returning default email.");
            return "unknown@example.com";
        });
위 코드에서는 orElseGet 을 사용하여, Optional 객체가 비어있을 경우에만 로그를 출력하고, "unknown@example.com "을 반환하도록 설정하였습니다.


내저장소 바로가기 luxury515

728x90

들어가면서…

  1. 복합키 사용하는 케이스
  • 여러 컬럼을 하나의 식별자로 사용해야 할 때(ex: 학생과 과목을 조인한 결과를 저장하는 테이블에서, 학생과 과목을 기준으로 각각 조회해야 할 때)
  1. 왜 복합키 사용해야 하는 지
  • 기본키로 단일 컬럼을 사용할 경우, 하나의 컬럼으로는 식별할 수 없는 경우가 있기 때문에 복합키를 사용해야 합니다.
  1. 복합키 사용하기 위한 사전 설정
  • @Embeddable 어노테이션을 사용하여 복합키 클래스를 정의합니다.
  • 복합키 클래스 내부에는 @Embeddable 어노테이션을 사용하지 않고 @Column 어노테이션으로 각 컬럼을 정의합니다.
  • 해당 엔티티 클래스의 @EmbeddedId 어노테이션을 사용하여 복합키 클래스를 매핑합니다.

코드 예시

  • 복합키 클래스 정의
@Embeddable
public class StudentSubjectId implements Serializable {
    private Long studentId;
    private Long subjectId;
    
    //생성자, getter, setter, equals, hashCode 생략
}
  • 엔티티 클래스 정의
@Entity
public class StudentSubject {
    @EmbeddedId
    private StudentSubjectId id;
    
    @ManyToOne
    @MapsId("studentId")
    private Student student;
    
    @ManyToOne
    @MapsId("subjectId")
    private Subject subject;
    
    //생성자, getter, setter 생략
}

복합키 장단점

장점:

  • 복합키를 사용하면 하나의 컬럼으로는 식별할 수 없는 경우에도 식별할 수 있습니다.
  • 복합키를 사용하면 여러 컬럼을 하나의 식별자로 사용할 수 있으므로, 테이블 간 조인 시 효율적인 쿼리를 작성할 수 있습니다.

단점:

  • 복합키를 사용하면 코드가 복잡해질 수 있습니다.
  • @IdClass 방식은 복합키를 구성하는 각각의 식별자 컬럼을 별도로 정의해야 하므로 유지보수성이 떨어질 수 있습니다.

결론

따라서 복합키를 사용할 때는 상황에 맞게 적절한 방법을 선택하고, 유지보수성을 고려하여 구현해야 합니다. ^^


내저장소 바로가기 luxury515

+ Recent posts