Mvitimin / Microservices_study

Study for MSA
0 stars 0 forks source link

JPA 리포지터리와 모델 구현 #10

Open Mvitimin opened 9 months ago

Mvitimin commented 9 months ago

JPA 리포지터리와 모델 구현

github

https://github.com/madvirus/ddd-start2

스프링 데이터 JPA 는 OrderRepository를 리포지터리로 인식해서 알맞게 구현한 객체를 스프링 빈으로 등록한다.

엔티티와 밸류기본매핑구현

애그리거트와 JPA 매핑을 위한 기본 규칙은 다음과 같다. 애그리거트 루트는 엔티티이므로 @Entity 로 설정한다

한테이블에 엔티티와 밸류 데이터가 같이 있다면 밸류는 @Embeddable 로 매핑 설정한다. 밸류 타입 프로퍼티는 @Embedded로 매핑 설정한다.

@Entity
@Table(name = "purchase_order")
@Access(AccessType.FIELD)
public class Order {
    @EmbeddedId
    private OrderNo number;

Order에 속하는 Orderer는 밸류이므로 @Embedabble로 매핑한다.

@Embeddable
public class Orderer {

    @AttributeOverrides(
            @AttributeOverride(name = "id", column = @Column(name = "orderer_id"))
    )
    private MemberId memberId;

    @Column(name = "orderer_name")
    private String name;

Orderer의 memberId는 Member 애그리거트를 ID로 참조한다. Member의 ID 타입으로 사용되는 MemberId는 다음과 같이 id 프로퍼티와 매핑되는 테이블 컬럼 이름으로 member_id를 지정하고있다.

@Embeddable
public class MemberId implements Serializable {
    @Column(name = "member_id")
    private String id;

@Embeddable
public class ShippingInfo {
    @Embedded
    @AttributeOverrides({
            @AttributeOverride(name = "zipCode", column = @Column(name = "shipping_zip_code")),
            @AttributeOverride(name = "address1", column = @Column(name = "shipping_addr1")),
            @AttributeOverride(name = "address2", column = @Column(name = "shipping_addr2"))
    })
    private Address address;
    @Column(name = "shipping_message")
    private String message;
    @Embedded
    private Receiver receiver;

루트 엔티티인 Order클래스는 @Embedded를 이용해서 밸류 타입 프로퍼티를 설정한다.


@Entity
@Table(name = "purchase_order")
@Access(AccessType.FIELD)
public class Order {
    @EmbeddedId
    private OrderNo number;

    @Version
    private long version;

    @Embedded
    private Orderer orderer;

    @ElementCollection(fetch = FetchType.LAZY)
    @CollectionTable(name = "order_line", joinColumns = @JoinColumn(name = "order_number"))
    @OrderColumn(name = "line_idx")
    private List<OrderLine> orderLines;

    @Convert(converter = MoneyConverter.class)
    @Column(name = "total_amounts")
    private Money totalAmounts;

    @Embedded
    private ShippingInfo shippingInfo;

기본생성자

엔티티와 밸류의 생성자는 객체를 생성할 때 필요한 것을 전달받는다. 예를 들어 Receiver 밸류 타입은 생성 시점에 수취인 이름과 연락처를 생성자 파라미터로 전달받는다.

@Embeddable
public class Receiver {
    @Column(name = "receiver_name")
    private String name;
    @Column(name = "receiver_phone")
    private String phone;

    public Receiver() {
    }

    public Receiver(String name, String phone) {
        this.name = name;
        this.phone = phone;
    }

Receiver가 불변타입이면 생성 시점에 필요한 값을 모두 전달 받으므로 값을 변경하는 set 메서드를 제공하지 않는다. 이는 Receiver 클래스에 기본 생성자를 추가할 필요가 없다는 것을 의미한다.

하지만 JPA에서 @Entity와 @Embeddable로 클래스를 매핑하려면 기본생성자를 제공해야한다. DB에서 데이터를 읽어와 매핑된 객체를 생성할 때 기본 생성자를 사용해서 객체를 생성하기 때문이다.

이런 기술적제약으로 인해 Receiver와 같은 불변타입은 기본 생성자가 필요없음에도 불구하고 기본생성자를 추가해야한다.

@Embeddable
public class Receiver {
    @Column(name = "receiver_name")
    private String name;
    @Column(name = "receiver_phone")
    private String phone;

    public Receiver() {
    }

    public Receiver(String name, String phone) {
        this.name = name;
        this.phone = phone;
    }

필드 접근방식

JPA는 필드와 메서드 두 가지 방식으로 매핑을 처리 할 수 있다. 메서드 방식을 사용하려면 get,set을 구현해야하지만 필드 접근은 안해도됨.

@Entity
@Table(name = "purchase_order")
@Access(AccessType.FIELD)
public class Order {
    @EmbeddedId
    private OrderNo number;

AttributeConverter 를 이용한 밸류 매핑 처리

int,long,String,LocaleDate 와 같은 타입은 DB xpdlqmfdml gksro zjffjadp aovld

public class Length {
  private int value;
  private String unit;
}

-----> (1000mm저장) WIDTH VARCHAR(200)


public class MoneyConverter implements AttributeConverter<Money, Integer> {

    @Override
    public Integer convertToDatabaseColumn(Money money) {
        return money == null ? null : money.getValue();
    }

    @Override
    public Money convertToEntityAttribute(Integer value) {
        return value == null ? null : new Money(value);
    }
}

convertToDatabaseColumn() => 밸류타입을 DB컬럼값으로 변환하는 기능 convertToEntityAttribute() => DB컬럼값을 밸류로 변환하는 기능

@Entity
@Table(name = "purchase_order")
@Access(AccessType.FIELD)
public class Order {

    @Convert(converter = MoneyConverter.class)
    @Column(name = "total_amounts")
    private Money totalAmounts;

밸류컬렉션: 한개 컬럼 매핑

밸류 컬렉션을 별도 테이블이 아닌 한개 컬럼에 저장해야할 때가 있다. 예를 들어 도메인 모델에는 이메일 주소 목록을 Set으로 보관하고 DB 에는 한개 컬럼에 콤마로 구분해서 저장해야할 때가있다. 이때 AttributeConverter를 사용하면 밸류컬렉션을 한 개 컬럼에 쉽게 매핑할 수 있다.

public class EmailSet {
    private Set<Email> emails = new HashSet<>();

    public EmailSet(Set<Email> emails) {
        this.emails.addAll(emails);
    }

    public Set<Email> getEmails() {
        return Collections.unmodifiableSet(emails);
    }

}

public class EmailSetConverter implements AttributeConverter<EmailSet, String> {
    @Override
    public String convertToDatabaseColumn(EmailSet attribute) {
        if (attribute == null) return null;
        return attribute.getEmails().stream()
                .map(email -> email.getAddress())
                .collect(Collectors.joining(","));
    }

    @Override
    public EmailSet convertToEntityAttribute(String dbData) {
        if (dbData == null) return null;
        String[] emails = dbData.split(",");
        Set<Email> emailSet = Arrays.stream(emails)
                .map(value -> new Email(value))
                .collect(toSet());
        return new EmailSet(emailSet);
    }
}

밸류를 이용한 ID 매핑

OderNo, MemberId등이 식별자를 표현하기 위해 사용한 밸류 타입이다. 밸류 타입을 식별자로 매핑하면 @Id 대신 @EmbeddedId 어노테이션을 사용한다.

@Entity
@Table(name = "purchase_order")
@Access(AccessType.FIELD)
public class Order {
    @EmbeddedId
    private OrderNo number;

@Embeddable
public class OrderNo implements Serializable {
    @Column(name = "order_number")
    private String number;

    protected OrderNo() {
    }

    public OrderNo(String number) {
        this.number = number;
    }

    public String getNumber() {
        return number;
    }

별도 테이블에 저장하는 밸류 매핑

밸류인지 엔티티인지 구분하는 방법은 고유 식별자를 갖는지 확인하는 것이다. 하지만 식별자를 찾을 때 매핑되는 테이블의 식별자를 애그리거트 구성요소의 식별자와 동일한 것으로 착각하면 안된다.

Artcle, ArticleContent가 ID로 서로 매핑되는 테이블이 있다하더라도 ArticleContent가 식별자를 가진다고해서 엔티티는 아니다. 이 식별자가 ArticleContent의 고유의 id가 아니라 매핑용 id기 때문이다. 그러므로 ArticleContent는 밸류이다.

@Entity
@Table(name = "article")
@SecondaryTable(
        name = "article_content",
        pkJoinColumns = @PrimaryKeyJoinColumn(name = "id")
)
public class Article {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;

    @AttributeOverrides({
            @AttributeOverride(
                    name = "content",
                    column = @Column(table = "article_content", name = "content")),
            @AttributeOverride(
                    name = "contentType",
                    column = @Column(table = "article_content", name = "content_type"))
    })
    @Embedded
    private ArticleContent content;

@SecondaryTable 의 name 속성은 밸류를 저장할 테이블을 지정한다. pkJoinColumns 속성은 밸류 테이블에서 엔티티 테이블로 조인할 때 사용할 컬럼을 지정한다.

밸류 컬렉션을 @Entity로 매핑하기.

product 라는 테이블과 image 라는 테이블이있다.

image, product 모두 각자 개별 식별자 고유 id가 있으나 image 테이블이 product 테이블 id를 매핑하려고함. 또한 Image 클래스는 상위 클래스, InternalImage, ExternalImage 두개의 클래스가 상속받음

그러면 Image 테이블에

1.@Inhritance 애너테이션을 적용 2.strategy 값으로 SINGLE_TABLE 사용 3.@DiscriminatorColumn 어노테이션을 이용하여 타입 구분용으로 사용할 컬럼 지정

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "image_type")
@Table(name = "image")
public abstract class Image {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "image_id")
    private Long id;

    @Column(name = "image_path")
    private String path;

    @Column(name = "upload_time")
    private LocalDateTime uploadTime;

    protected Image() {
    }

    public Image(String path) {
        this.path = path;
        this.uploadTime = LocalDateTime.now();
    }

    protected String getPath() {
        return path;
    }

    public LocalDateTime getUploadTime() {
        return uploadTime;
    }

    public abstract String getUrl();

    public abstract boolean hasThumbnail();

    public abstract String getThumbnailUrl();

}
@Entity
@DiscriminatorValue("II")
public class InternalImage extends Image {
    protected InternalImage() {
    }

@Entity
@DiscriminatorValue("EI")
public class ExternalImage extends Image {
    protected ExternalImage() {
    }

Image는 밸류이므로 독자적인 라이프 사이클을 갖지 않고 Product에 의존한다. Product를 저장할 때 함께 저장되고 PRoduct를 삭제할때 함께 삭제되도록 cascade속성지정한다. 리스트에서 Image 객체를 제거하면 DB에서 함께 삭제되도록 orphanRemoval 도 true로 설정한다.

@Entity
@Table(name = "product")
public class Product {
    @EmbeddedId
    private ProductId id;

    @ElementCollection(fetch = FetchType.LAZY)
    @CollectionTable(name = "product_category",
            joinColumns = @JoinColumn(name = "product_id"))
    private Set<CategoryId> categoryIds;

    private String name;

    @Convert(converter = MoneyConverter.class)
    private Money price;

    private String detail;

    @OneToMany(cascade = {CascadeType.PERSIST, CascadeType.REMOVE},
            orphanRemoval = true, fetch = FetchType.LAZY)
    @JoinColumn(name = "product_id")
    @OrderColumn(name = "list_idx")
    private List<Image> images = new ArrayList<>();

clear 메소드를 실행하게되면 select * from image where product_id = ? 쿼리와 각 image를 삭제하기 위한 네번의 delete from image where image_id = ? 쿼리를 실행한다. 변경 빈도가 낮으면 괜찮지만 빈도가 높으면 전체 서비스 성능에 문제가 될 수 있다.

    public void changeImages(List<Image> newImages) {
        images.clear();
        images.addAll(newImages);
    }

하이버네이트는 @Embeddable 타입에 대한 컬렉션의 clear() 메서드를 호출하면 컬렉션에 속한 객체를 로딩하지 않고 한 번의 delete 쿼리로 삭제처리를 수행하게 된다. @Embeddable 로 매핑된 단일 클래스로 구현해야한다.

if else 를 써야함


if (imageType.equals("II") {
    return true;
} else {
    return false;
}

참조와 조인 테이블을 이용한 단방향 M-N매핑


@Entity
@Table(name = "product")
public class Product {
    @EmbeddedId
    private ProductId id;

    @ElementCollection(fetch = FetchType.LAZY)
    @CollectionTable(name = "product_category",
            joinColumns = @JoinColumn(name = "product_id"))
    private Set<CategoryId> categoryIds;

M-N연관은 밸류 컬렉션 매핑과 동일한 방식이긴 하지만 차이점이 있다면 집합 값에 밸류대신 연관을 맺는 식별자가 온다는 점. product를 삭제하면 사용한 조인 테이블 데이터도 삭제된다.

애그리거트 영속성 전파

@Embeddable 매핑 타입은 함께 저장되고 삭제되므로 cascade 속성을 추가로 설정하지 않아도된다. 반면에 애그리거트에 속한 @Entity 타입에 대한 매핑은 cascade속성을 사용해서 삭제시에 함께 처리되도록 해야한다.

    @OneToMany(cascade = {CascadeType.PERSIST, CascadeType.REMOVE},
            orphanRemoval = true, fetch = FetchType.LAZY)
    @JoinColumn(name = "product_id")
    @OrderColumn(name = "list_idx")
    private List<Image> images = new ArrayList<>();

식별자 생성기능

@Entity
@Table(name = "article")
@SecondaryTable(
        name = "article_content",
        pkJoinColumns = @PrimaryKeyJoinColumn(name = "id")
)
public class Article {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
Mvitimin commented 8 months ago

자바 ORM 표준 JPA 프로그래밍

https://github.com/holyeye/jpabook

3장 영속성 관리

DynamicUpdate

JPA 기본전략은 엔티티의 모든 필드를 업데이트 하게된다. 컬럼이 대략 30개이상이 된다면 기본 방법인 정적 수정 쿼리보다, @DynamicUpdate를 사용한 동적 쿼리수정이 훨씬 빠르다.

https://www.baeldung.com/spring-data-jpa-dynamicupdate

준영속

detach : 특정 엔티티만 준영속 상태 영속성 컨텍스트에게 해당 엔티티를 더이상 관리하지 말라는 뜻 변경감지, 데이터베이스 반영 모두 되지않음 지연로딩도 할 수 없게된다.

4장 엔티티 매핑

@Entity
@Table(name="MEMBER", uniqueConstraints = {@UniqueConstraint( //추가 //**
        name = "NAME_AGE_UNIQUE",
        columnNames = {"NAME", "AGE"} )})
public class Member {

    @Id
    @Column(name = "ID")
    private String id;

    @Column(name = "NAME", nullable = false, length = 10) //추가 //**
//    @Column(name = "NAME") //추가 //**
    private String username;

    private Integer age;

    //=== 추가
    @Enumerated(EnumType.STRING)
    private RoleType roleType;

    @Temporal(TemporalType.TIMESTAMP)
    private Date createdDate;

    @Temporal(TemporalType.TIMESTAMP)
    private Date lastModifiedDate;

    @Lob
    private String description;

    @Transient
    private String temp;

    //Getter, Setter

    public String getId() {
        return id;
    }

기본 키 매핑

기본키를 직접 할당하려면 @Id만 사용하면 됨. 자동 생성 전략을 사용하려면 @Id에 @GeneratedValue를 추가하고 원하는 키 생성 전략을 선택하면 된다.


 @Id
    @GeneratedValue
    @Column(name = "ITEM_ID")
    private Long id;

@Access

JPA가 엔티티 데이터에 접근하는 방식을 지정

@Entity
@Access(AccessType.FIELD)
public class Member {
    @Id
    private String id;
}

@Id가 필드에있으면 굳이 Field 설정안해도됨

@Entity
@Access(AccessType.PROPERTY)
public class Member {

    private String id;

    @Id
    public String getId(){
        return id;
    }
}

함께 사용도 가능하다.

@Entity
public class Member {

    @Id
    private String id;

    @Trasient
    private Stirng firstName;

    @Trasient
    private Stirng lastName;

    private String fullName;

    @Access(AccessType.PROPERTY)
    public String getFullName() {
        return firstName + lastName;
    }

}

5장 연관관계 매핑 기초

단방향 연관관계

연관관계 중에선 다대일 (N:1) 단방향 관계를 가장 먼저 이해해야 한다. Order: N , Member: 1

@Entity
@Table(name = "ORDERS")
public class Order {

    @Id @GeneratedValue
    @Column(name = "ORDER_ID")
    private Long id;

    @ManyToOne
    @JoinColumn(name = "MEMBER_ID")
    private Member member;      //주문 회원

양방향 연관관계

@Entity
public class Member {

    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

    private String name;

    private String city;
    private String street;
    private String zipcode;

    @OneToMany(mappedBy = "member")
    private List<Order> orders = new ArrayList<Order>();

6장 다양한 연관관계 매핑

일대일

@Entity
@Table(name = "ORDERS")
public class Order extends BaseEntity {

    @Id
    @GeneratedValue
    @Column(name = "ORDER_ID")
    private Long id;

    @ManyToOne
    @JoinColumn(name = "MEMBER_ID")
    private Member member;      //주문 회원

    @OneToMany(mappedBy = "order")
    private List<OrderItem> orderItems = new ArrayList<OrderItem>();

    @OneToOne
    @JoinColumn(name = "DELIVERY_ID")
    private Delivery delivery;  //배송정보
@Entity
public class Delivery {

    @Id @GeneratedValue
    @Column(name = "DELIVERY_ID")
    private Long id;

    @OneToOne(mappedBy = "delivery")
    private Order order;

    private String city;
    private String street;
    private String zipcode;

다대다

@Entity
public class Item {

    @Id
    @GeneratedValue
    @Column(name = "ITEM_ID")
    private Long id;

    private String name;        //이름
    private int price;          //가격
    private int stockQuantity;  //재고수량

    @ManyToMany(mappedBy = "items")                         //**
    private List<Category> categories = new ArrayList<Category>(); //**
@Entity
public class Category {

    @Id @GeneratedValue
    @Column(name = "CATEGORY_ID")
    private Long id;

    private String name;

    @ManyToMany
    @JoinTable(name = "CATEGORY_ITEM",
            joinColumns = @JoinColumn(name = "CATEGORY_ID"),
            inverseJoinColumns = @JoinColumn(name = "ITEM_ID"))
    private List<Item> items = new ArrayList<Item>();

복합 기본 키

@Data
public class UserGroupMemberKey implements Serializable {
  private String groupId;
  private String userId;
}
@Data
@Entity
@NoArgsConstructor
@IdClass(UserGroupMemberKey.class)
public class UserGroupMember {
  @Id
  private String groupId;
  @Id
  private String userId;
}

복합 기본 키

다대다 매핑

@Entity
public class Category {

    @Id @GeneratedValue
    @Column(name = "CATEGORY_ID")
    private Long id;

    private String name;

    @ManyToMany
    @JoinTable(name = "CATEGORY_ITEM",
            joinColumns = @JoinColumn(name = "CATEGORY_ID"),
            inverseJoinColumns = @JoinColumn(name = "ITEM_ID"))
    private List<Item> items = new ArrayList<Item>();

    @ManyToOne
    @JoinColumn(name = "PARENT_ID")
    private Category parent;

    @OneToMany(mappedBy = "parent")
    private List<Category> child = new ArrayList<Category>();
@Entity
public class Item {

    @Id
    @GeneratedValue
    @Column(name = "ITEM_ID")
    private Long id;

    private String name;        //이름
    private int price;          //가격
    private int stockQuantity;  //재고수량

    @ManyToMany(mappedBy = "items")                         //**
    private List<Category> categories = new ArrayList<Category>(); //**
@Entity
public class Delivery {

    @Id @GeneratedValue
    @Column(name = "DELIVERY_ID")
    private Long id;

    @OneToOne(mappedBy = "delivery")
    private Order order;

    private String city;
    private String street;
    private String zipcode;

    @Enumerated(EnumType.STRING)
    private DeliveryStatus status; //ENUM [READY(준비), COMP(배송)]
Mvitimin commented 8 months ago

8장 프록시와 연관관계 관리

즉시로딩과 지연로딩

@Entity
@Table(name = "ORDERS")
public class Order {

    @Id @GeneratedValue
    @Column(name = "ORDER_ID")
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "MEMBER_ID")
    private Member member;      //주문 회원

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
    private List<OrderItem> orderItems = new ArrayList<OrderItem>();

    @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    @JoinColumn(name = "DELIVERY_ID")
    private Delivery delivery;  //배송정보

컬렉션에 FetchType.EAGER 사용시 주의점

  1. ManyToOne, OneToOne

    • Optional == false : 내부조인
    • Optional == true : 외부조인
  2. ManyToMany, OneToMany

    • Optional == false : 외부조인
    • Optional == true: 외부조인

영속성 전이 : CASCADE

@Entity
@Table(name = "ORDERS")
public class Order {

    @Id @GeneratedValue
    @Column(name = "ORDER_ID")
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "MEMBER_ID")
    private Member member;      //주문 회원

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
    private List<OrderItem> orderItems = new ArrayList<OrderItem>();

    @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    @JoinColumn(name = "DELIVERY_ID")
    private Delivery delivery;  //배송정보

고아 객체

JPA는 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능을 제공 (ORPHAN) 부모 엔티티 컬렉션에서 자식 엔티티 참조만 제거하면 자식 엔티티가 자동으로 삭제 되게 함

parent1.getChildren().remove(0); // 자식엔티티를 컬렉션에서 제거

@OneToMany(mappedBy = "parents", orphanRemoval = true)

DELETE SQL이 실행되게 됨

@OneToMany 안에서만 사용할 수 있으며

부모를 제거하면 자식도 같이 제거되기 때문에 CascadeType.REMOVE를 설정한것과 같다.

Mvitimin commented 8 months ago

12장 스프링 데이터 JPA

https://docs.spring.io/spring-data/jpa/docs/1.8.0.RELEASE/reference/html/

Keyword Sample JPQL snippet
And findByLastnameAndFirstname … where x.lastname = ?1 and x.firstname = ?2
Or findByLastnameOrFirstname … where x.lastname = ?1 or x.firstname = ?2
Is,Equals findByFirstname,findByFirstnameIs,findByFirstnameEquals … where x.firstname = 1?
Between findByStartDateBetween … where x.startDate between 1? and ?2
LessThan findByAgeLessThan … where x.age < ?1
LessThanEqual findByAgeLessThanEqual … where x.age ⇐ ?1
GreaterThan findByAgeGreaterThan … where x.age > ?1
GreaterThanEqual findByAgeGreaterThanEqual … where x.age >= ?1
After findByStartDateAfter … where x.startDate > ?1
Before findByStartDateBefore … where x.startDate < ?1
IsNull findByAgeIsNull … where x.age is null
IsNotNull,NotNull findByAge(Is)NotNull … where x.age not null
Like findByFirstnameLike … where x.firstname like ?1
NotLike findByFirstnameNotLike … where x.firstname not like ?1
StartingWith findByFirstnameStartingWith … where x.firstname like ?1 (parameter bound with appended %)
EndingWith findByFirstnameEndingWith … where x.firstname like ?1 (parameter bound with prepended %)
Containing findByFirstnameContaining … where x.firstname like ?1 (parameter bound wrapped in %)
OrderBy findByAgeOrderByLastnameDesc … where x.age = ?1 order by x.lastname desc
Not findByLastnameNot … where x.lastname <> ?1
In findByAgeIn(Collection ages) … where x.age in ?1
NotIn findByAgeNotIn(Collection age) … where x.age not in ?1
True findByActiveTrue() … where x.active = true
False findByActiveFalse() … where x.active = false
IgnoreCase findByFirstnameIgnoreCase … where UPPER(x.firstame) = UPPER(?1)

JPA NamedQuery

쿼리에 이름을 부여해 사용하는 방법
https://www.baeldung.com/hibernate-named-query

@Query, 리포지토리 메소드에 쿼리 정의

@Query(
  value = "SELECT * FROM USERS u WHERE u.status = 1", 
  nativeQuery = true)
Collection<User> findAllActiveUsersNative();

파라미터 바인딩

https://www.baeldung.com/hibernate-named-query

@Query("SELECT u FROM User u WHERE u.status = :status and u.name = :name")
User findUserByStatusAndNameNamedParams(
  @Param("status") Integer status, 
  @Param("name") String name);

명세 적용

https://www.baeldung.com/rest-api-search-language-spring-data-specifications