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 값에만 적용할수 있는거라서 적용할수가 없었다.

자동화 테스트를 하기위해 만들어졌다는 Selenium 으로 자동 로그인을 해보자.

 

1. 크롬드라이버 설치

 

2. Selenium Library Import 

 byte-buddy-1.8.15.jar
 client-combined-3.141.59.jar
 commons-exec-1.3.jar
 guava-25.0-jre.jar
 okhttp-3.11.0.jar
 okio-1.14.0.jar

3. System 셋팅  & 네이버로 로그인페이지로 이동

 

System.setProperty("webdriver.chrome.driver", "C:/chromedriver.exe");

WebDriver driver1 = new ChromeDriver();

driver.get("https://nid.naver.com/nidlogin.login?mode=form&url=https%3A%2F%2Fwww.naver.com"); //네이버로그인

 

4. 아이디 넣고, 비밀번호 넣고 Submit! 하면 끝날줄 알았는데.... 자동로그인방지에 걸리네용.

 

WebElement webElement = driver.findElement(By.id("id"));

webElement.sendKeys("value", "네이버아이디");

pasteClipBoard(webElement , naverId , driver);
Thread.sleep(1000);

webElement = driver.findElement(By.id("pw"));
webElement.sendKeys("value", "네이버비밀번호");

Thread.sleep(1000);

webElement = driver.findElement(By.id("log.login"));
webElement.submit();

 

5. 아이디. 비밀번호 입력을 클립보드를 이용해 넣으면, 자동로그인에 걸리지 않는다!.

4번을 수정해보자.

/** 로그인 시작 */
WebElement webElement = driver.findElement(By.id("id"));
pasteClipBoard(webElement , naverId , driver);
Thread.sleep(1000);

webElement = driver.findElement(By.id("pw"));
pasteClipBoard(webElement , naverPassWord, driver);
Thread.sleep(1000);
webElement = driver.findElement(By.id("log.login"));
webElement.submit();

 

/**
* 클립보드 복사
* @param webElement
* @param clipBoardString
*/
public void pasteClipBoard(WebElement webElement , String clipBoardString , WebDriver driver){
Actions actions = new Actions(driver);
webElement.click();
StringSelection stringSelection = new StringSelection(clipBoardString);
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
clipboard.setContents(stringSelection, null);
actions.keyDown(webElement, Keys.LEFT_CONTROL).perform();
actions.sendKeys(webElement,"v").perform();
}

디폴트 설정으로는 http status code 가 200에서 300 사이가 아니면 에러가 발생하여 , 404 일때 에러가 아닌 정상적인 값으로 판단하고 싶을때 처리.

 

1. FeignClient  로 데이터 찾기 호출시 404 에러가 발생하여 ResponseEntity<Member> 대신에 Optional<Member> 로 처리.

2. @FeignClient 속성 decode404 = true

Before:

@FeignClient(name = "member", url = "${service-urls.xxxxxx}", configuration = FeignAutoConfiguration.class, contextId = "member")

public interface MemberFeignClient {

 

 

@GetMapping(value ="/members/search/findFirstByEmpId")

Entity<Member> findFirstByEmpId(@RequestParam("empId") String empId);

 

 

}

 

 

 

 

After:

@FeignClient(name = "member", url = "${service-urls.xxxxxx}", configuration = FeignAutoConfiguration.class, contextId = "member" , decode404 = true)

public interface MemberFeignClient {

 

 

@GetMapping(value ="/members/search/findFirstByEmpId")

Optional<Member> findFirstByEmpId(@RequestParam("empId") String empId);

 

 

}

프론트에서 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 ;
}

+ Recent posts