JPA , DDD 로 차세대 개발을 하면서 TOBE PK 와 별개로 AS-IS PK 체계를 가져가야하는 경우가있어서

데이터와 동일하게 채번("S" + sequence.nextval) 해야 하는   을 해야하는 경우 어떻게 해야하는지 찾아보다가

아래와같은 경우를 찾음.

 

@Id 가 붙지않는(pk 가 아닌) 데이터를 채번해야하는 경우

 

참고 : https://stackoverflow.com/questions/277630/hibernate-jpa-sequence-non-id

 

Hibernate JPA Sequence (non-Id)

Is it possible to use a DB sequence for some column that is not the identifier/is not part of a composite identifier? I'm using hibernate as jpa provider, and I have a table that has some columns...

stackoverflow.com

1번 (별로... 불필요한 Entity 추가로 쓰레기 테이블 생김)

필요한 데이터를 채번하는 Entity 추가해서 , Entity를 OneToOne 관계를 맺어서 추가할때마다 Entity에 추가하도록 한다.

 

2번. (적용한것) 어노테이션 + AOP 를 활용 ,

1. 적용할 필드 위에 어노테이션 붙인다.

2. 어노테이션 이 적용된 데이터를 set 해주는데, 이때 DB에서 채번한 데이터를 가져와서 조합시켰음.

 

3번.  @Column(columnDefinition="serial") 사용. 

 나의 경우에는 단순 채번데이터가 아닌 String + 채번된 값 이여서 상황에 맞지않아서 고려하지 않음.

 

4번. @GeneratedValue , @GenericGenerator 를 활용한 방법은 @Id 어노테이션이 붙은 pk 값에만 적용할수 있는거라서 적용할수가 없었다.

프론트에서 Member 정보를 caching 하기에 데이터가 너무 많아, 데이터정보를 최소화 하려고한다.

(_link , 불필요한 항복) 등 을 삭제처리 하려고 한다.

 

1.findAllBy  API 에 대해서 경량화가 필요할때(Front에서 전체조회하여 AutoComplete 에 사용) 가 있고, 아닐때가 있어서 Dynamic한 Projection 적용이 필요하다.

 

public interface MemberRepository extends

        JpaRepository<Member, Long>

        , QuerydslPredicateExecutor<Member>

        , QuerydslBinderCustomizer<QMember> {

 

 

    <T> List<T> findAllBy(Class<T> type);

 

 

}

 

 

memberRepository.findAllBy(MemberProjection4FrontCache.class);

@Projection(name="memberProjection4FrontCache", types = { Member.class })

public interface MemberProjection4FrontCache {

    Long getId();

 

    String getKoreanName();

 

    Reference<Category> getPositionCode();

 

    OnlyDownloadUrlProjection getProfileImage();

 

    List<OnlyOrganizationProjection> getDepartmentList();

}

 

 

 

2.Response  _embedded 에 불필요한 내용 삭제

produces = "application/x-spring-data-verbose+json"

@RestController

@RequestMapping(value = "/members")

@RequiredArgsConstructor

public class MemberController {

    private final MemberLogic memberLogic;

 

    // application/x-spring-data-verbose+json : _link 등 hateoas 관련된거 제거.

    @GetMapping(value="findAllMemberWithoutLink" , produces = "application/x-spring-data-verbose+json")

    public List<MemberProjection4FrontCache> findAllMemberWithoutLink(){

        return memberLogic.findAllBy(MemberProjection4FrontCache.class);

    }

 

}

 

 

Spring data rest 에서 query parameter 로 is null 이나 is not null 을 하고싶었는데, 

"http://localhost:1000/organizations?parent=IsNull"  처럼 호출해서

 

"select * from organization where parent is null" 와 같은 쿼리를 호출하고 싶어서 찾아보니

"Perhaps in the future, we'll be able to specify how to interpret null parameters using the @NullMeans annotation.  Notice that it's a proposed feature at this time and is still under consideration."

 

미래에 ..... 사용할수 있을거라한다.

 

(참고 꼼수 : http://localhost:1000/organizations?parent=-999 처럼 없는id값을 넣으니까  JPA 에서 ID로 조회를 해보고 없으니까 IS NULL 로 조회를 했다). Not NUll 도 할수있는 꼼수라도 있으면 댓글 부탁드립니다.

 

지금은 아래처럼.... 하라고 포스팅에서 말해주고있다....

    @Query("SELECT c FROM Customer c WHERE (:name is null or c.name = :name) and (:email is null or c.email = :email)")
    List<Customer> findCustomerByNameAndEmail(@Param("name") String name, @Param("email") String email);

www.baeldung.com/spring-data-jpa-null-parameters

 

Spring Data JPA and Null Parameters | Baeldung

Learn different ways to use null parameters with Spring Data JPA queries, including how to make query parameters optional.

www.baeldung.com

 

 

Projection 언제사용하나요?

> Spring data jpa 사용시 Reponse 항목에서 모두보여주고 싶지 않을때 사용한다.

 

어떻게 사용하나요?

>

1. 인터페이스 생성

2. @Projection 어노테이션

3. name 설정(interface 이름과 동일하게 하는게 가독성이 좋을듯싶다.)

4. types = "jpa Entity 클래스명"

5. 내가 보여주고싶은 항목의 getter 메소드 선언

@Projection(name="membersWithOrganizationProjection", types = { Member.class })
public interface MembersWithOrganizationProjection {

    Long getId();

    String getKoreanName();
    
 }

Member 를 REST API 사용시 : localhost:8080/members?projection=membersWithOrganizationProjection

 

Member 안 필드에 다른 오브젝트가 있는데 그 오브젝트안에 있는 필드도 프로젝션이 가능한가요?

 

Member - OrganizationMember - Organization 조인관계에서 세개의 오브젝트에서 Projection 적용해보겠습니다.

 

클래스 조합은 아래참조

참조 : JPA 다대다 관계 풀기(1)

 

JPA 다대다 관계 풀기(1)

1장에서는 엔티티 정보에 선언에 대해 알아볼 것이다. 아래는 클래스 다이어그램, ERD 구성이다. 예시) Organization(조직)- OrganizationMember(조직멤버 구성정보) - Member(구성원:조직에는 N명의 멤버들이

endless-learn-code.tistory.com

MembersWithOrganizationProjection - OrganizationMemberProjection - OrganizationProjection 

@Projection(name="membersWithOrganizationProjection", types = { Member.class })
public interface MembersWithOrganizationProjection {

    Long getId();

    String getKoreanName();
    /* 리턴형태에 Projetion Interface 값을 추가하였습니다. */
    List<OrganizationMemberProjection> getOrganizationMembers();
 }
@Projection(name="organizationMemberProjection", types = { OrganizationMember.class })
public interface OrganizationMemberProjection {
	/** Projection Inferface 내에 메소드 리턴형태가  */
    OrganizationProjection getOrganization();

    Boolean getMainOrganization();
}
@Projection(name="organizationProjection", types = { Organization.class })
public interface OrganizationProjection {

    Long getId();

    String getOrganizationCode();

    String getKoreanName();

}

JPA쓰면서 느낀점 : DB를 내가 정하고 잘정규화되어서 간단한 쿼리만 나오는 경우가 아니라면 mybatis 쓰는게 나을거 같다.

유지보수 하기도 힘들거같다. ㅠㅠ 내가 잘못쓰는건가........

 

추가 두번째 프로젝트에서 느낀점 : Spring data rest를 같이쓰면 도메인 모델에 대한 CRUD 개발속도가 엄청 빠르다! (swagger로 Repository 에 메소드 추가할때마다 API 추가되는거 처음에 보면 깜놀합니다)

 

querydsl value operator 를 사용하니 1테이블 조회할땐 다양하게 검색조건을 후딱 넣어서 검색할수 있음.

 

SI 프로젝트에서 개발자 러닝커브가 아직까지는 매우심함.

DDD 로 개발을 한다고 하지만, 화면 설계를 설계자(DDD 를 이해못한)가 통계성 화면조회에서 Entity 여러개에 있는 모든항목 다 그리드에 보여줘야된다고 하면 개발자는 고뇌에 빠짐.

 

불필요한 항목,검색조건이라면 백앤드개발자가 기획,PO 를 옆에 두고 화면을 적절하게 수정해야 되는데, SI 에서는 개발자가 그런 회의하는거 자체가 이상하다 생각하는거 같음)

 

현재 TA 가이드 안내로는 4개의 테이블을 조인해야된다고 한다면, 4개의 Entity 에 이벤트를 발생해 1개의 통계성? Entity에 변경을 적용하는 방법을 이용한다고 한다. 

 

(제 생각에는 통계성과 같이 정확한 데이터가 딱 떨어지는게 필요하지 않은 데이터(카운트나,자릿수가 큰 SUM값) 에서는 적절하나, 그냥 (상품,주문,결제,등등 엔티티가 많은 그리드화면이라고 무작정 적용하는건 아닌거같습니다. 이런화면 자체가 DDD,JPA 에는 맞지 않은거 같음)

백오피스성 화면에 JPA 자체가 맞지 않는거 아닌가 싶음.)

 

JPA 고급스킬을 써서 N개의테이블을 조인이 되긴하나 , select * from a,b,c where a.x=1 and b.x = 2 and c.x =3 

같은 쿼리를 생성할수는 없기떄문에, 관리자(백오피스) 화면에는 JPA 로는 개발하기 힘든 부분이 많다.)

1장에서는 엔티티 정보에 선언에 대해 알아볼 것이다. 아래는 클래스 다이어그램, ERD 구성이다.

예시) Organization(조직)- OrganizationMember(조직멤버 구성정보) - Member(구성원:조직에는 N명의 멤버들이 있을수 있고, 멤버는 여러조직에 가입할수 있다.

 

단 조직에 들어간일자, 나온일자, 주조직 여부 등의 정보를 관리해야 되어야 하기도 하고,

실무에서 ManyToMany 를 사용하는건 문제가 많이 생긴다는건 이미 많이 올라와 있다고 해서 생략!

그리고 여기에서는 mainOrganization 하나만 썼지만 실제 OrganizationMember 안에 여러컬럼이 더 추가로 필요로 하였다.

 

1:N -> 조직 : 조직멤버(구성정보)

1:N -> 구성원 : 조직멤버(구성정보)

 

아래그림으로 관계는 이해할수 있으리라 생각됩니다.

 

 

 

코드는 한번 훑어보구 2장부터 어노테이션 및 하나하나 풀어보겠습니다.

@Data
@SequenceGenerator(name = "default", sequenceName = "organization_seq", allocationSize = 1)
@Entity
@NamedEntityGraph(name = "graph.OrganizationWithMember", attributeNodes = @NamedAttributeNode(value = "organizationMembers",subgraph = "subgraph.organizationMembers"), subgraphs = {@NamedSubgraph(name = "subgraph.organizationMembers", attributeNodes = @NamedAttributeNode(value = "member", subgraph = "subgraph.member"))})
public class Organization{

    @Id
    private Long id;

    private String organizationCode;

    private String koreanName;

    // 1:N -> 조직 : 조직멤버(구성정보)
    @JsonProperty(access=JsonProperty.Access.READ_ONLY)
    @org.hibernate.annotations.Fetch(FetchMode.SUBSELECT)
    @OneToMany(mappedBy = "organization")
    private List<OrganizationMember> organizationMembers;

}

 

@Data
@SequenceGenerator(name = "default", sequenceName = "organization_member_seq", allocationSize = 1)
@Entity
@NamedEntityGraph(name = "OrganizationMembers.memberList", attributeNodes = @NamedAttributeNode("member"))
public class OrganizationMember {

    @Id
    Long id;

    @ManyToOne
    @JsonProperty(access=JsonProperty.Access.READ_ONLY)
    @JoinColumn(name = "organization_id")
    @MapsId("organization_id")
    @AttributeOverride(name="id", column=@Column(name="organization_id"))
    private Organization organization;

    @ManyToOne
    @JsonProperty(access=JsonProperty.Access.READ_ONLY)
    @JoinColumn(name = "member_id")
    @MapsId("member_id")
    @AttributeOverride(name="id", column=@Column(name="member_id"))
    private Member member;

    private Boolean mainOrganization;

}
import com.fasterxml.jackson.annotation.JsonProperty;
import jdk.jfr.Name;
import lombok.Data;
import org.hibernate.annotations.FetchMode;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

@Data
@SequenceGenerator(name = "default", sequenceName = "member_seq", allocationSize = 1)
//조인을 하기위한 서술
@NamedEntityGraph(name = "graph.MemberWithMainOrganization"
        , attributeNodes = @NamedAttributeNode(value = "organizationMembers",subgraph = "subgraph.organizationMembers")
        , subgraphs = {@NamedSubgraph(name = "subgraph.organizationMembers"
        , attributeNodes = @NamedAttributeNode(value = "organization", subgraph = "subgraph.organization"))})
@Entity
public class Member {

    @Id
    private Long id;

    private String empId;

    private String koreanName;

    @JsonProperty(access=JsonProperty.Access.READ_ONLY)
    @org.hibernate.annotations.Fetch(FetchMode.SUBSELECT)
    @OneToMany(mappedBy = "member")
    private List<OrganizationMember> organizationMembers ;
}

======================================================================================================================================================================

 

결론 : 1.조인해서 한번의 Query 로 Load 하려면 @EntityGraph 를 사용하여야 한다.

2. @NamedEntityGraph 선언할때 @NamedAttributeNode 에 원쿼리로 가져올 항목을 명시한다.

3. Repository 메소드에 @EntityGraph 선언하여 value에 사용할 @NamedEntityGraph 명을 적고, type 은 = EntityGraph.EntityGraphType. LOAD , FETCH 선택 사용.

4. 조인해서 가져오는 항목외에 다른 객체필드에 LAZY ,EAGER 를 선언해서 적어준다.

 

자세한 내용은 아래 참조.

======================================================================================================================================================================

 

엔티티 필드에 사용하는 LAZY, EAGER 를 Repository(메소드) 마다 다르게 사용하기 위해서 @NamedEntityGraph , @EntityGraph 를 사용한다.

@NameEntityGraph [  name  = 이름을 선언 , attributeNodes = 조인해서 가져올 항목들 선언 ]

@EntityGraph [ value = NameEntityGraph name 값 , type = FETCH,LOAD 선택 ] 

 

  • FETCH: entity graph에 명시한 attribute는 EAGER로 패치하고, 나머지 attribute는 LAZY로 패치
  • LOAD: entity graph에 명시한 attribute는 EAGER로 패치하고, 나머지 attribute는 entity에 명시한 fetch type이나 디폴트 FetchType으로 패치 (e.g. @OneToMany는 LAZY, @ManyToOne은 EAGER 등이 디폴트이다.)

* 이슈가 생겨서 쿼리수정이 필요할때

FETCH 로 선언했었을때 : EAGER 로 변경하고 싶을때 attribute 에 추가, LAZY 로 변경하고싶을경우 attribute에서 제거.

LOAD 로 선언했었을때  : EAGER 로 변경하고 싶을때 attribute 에 추가, LAZY 로 변경하고싶을경우 Entity 속성을 수정.

(여기서 Entity를 수정하는건 다른 API에 영향이 가기 때문에, FETCH 로 선언을 해서 사용하는게 추후 쿼리 이슈가 발생 했을경우 수정이 자유롭지 않을까 싶다.)

 


참고 : attributePaths 는 value 를 사용안하고 바로 필드를 적을때 사용.(value를 사용하는게 명시적으로 좋을듯하다) , subgraphs ( 2개이상 엔티티 조인할때 필요 )

 

Organization.java

@NamedEntityGraph(name = "Organization.subOrgList", attributeNodes = {@NamedAttributeNode("subOrganizationList")},subgraphs = {@NamedSubgraph(name = "subOrg",attributeNodes = @NamedAttributeNode("subOrganizationList"))})

public class Organization extends AbstractEntity implements AggregateRoot {

    ...................

    ...................

}

 

OrganizationRepository.java

@RepositoryRestResource

public interface OrganizationRepository extends

        JpaRepository<Organization, Long>, QuerydslPredicateExecutor<Organization>

        , QuerydslBinderCustomizer<QOrganization> {

 

    @EntityGraph(value = "Organization.subOrgList",attributePaths = {"parent"},type = EntityGraph.EntityGraphType.LOAD)

    Optional<Organization> findFirstByOrganizationCode(String organizationCode);

 

    @EntityGraph(value = "Organization.subOrgList",attributePaths = {"parent"},type = EntityGraph.EntityGraphType.LOAD)

    Page<Organization> findAll(Pageable pageable);

 

    List<Organization> findAllBy();

    ....

}

 

========================================================================================================================================================

LAZY , EAGER 차이

fetch= FetchType.LAZY:데이터를 가져다 쓰는 시점에 쿼리를 실행한다. ( 즉 projection 으로 사용하지 않는 필드를 조회하는 API 를 호출할땐 쿼리가 실행되지 않음 )

 

fetch= FetchType.EAGER:데이터를 가져옴 조인해서 가져온다.( 단 Page<Entity> findAll(Pageable pageable) 메소드에는 적용되지 않음. @EntityGraph 사용해줘야됨 )

 

 

public class Organization extends AbstractEntity implements AggregateRoot {

    .....................

    .....................

 

 

    @OneToMany(mappedBy="parent", fetch= FetchType.LAZY, cascade=CascadeType.ALL)

    @RestResource(exported=false)

    @JsonManagedReference

    public List<Organization> subOrganizationList;

}

 

ex) projection 으로  subOrganizationList

@Entity 를 넣으면 @Id 가 필수로 들어가야 된다. (pk 를 인식하기위해)

 

@Id 로 들어가있는값이 save(id) 가 될경우 id값이 기존에 들어가있으면 update( merge() ) 가 처리가 되고 기존에 없는값이면 insert( persist() ) 처리가 된다.

 

즉 PK가 없이 계속 insert 를 하려고 했을경우, 다른방법을 사용해야 한다.

 

전 native 쿼리를 사용해서 insert 문을 적었습니다.

 

 

+ Recent posts