readonly?
department - line 관계를 맺고싶을때?
어떤 department가있다. 이department가 포함된 market - factory의 어떤 생산 line에서 제품을만든다
강결합정도에 따라
상호 참조 하지않고 service에서 로직으로 연관데이터 조합
readonly로 데이터 참조
read & write가능 (-> 지양하기, 하위 도메인취급이 되어버림)
보수적으로 설계 -> 필요할때 하나씩 열어주는 방식으로 고려하면 :+1:
case 1 기존 rule대로 약결합으로 구현하면 생기는 문제
merge data -> stream / loop
의존성은 약하지만 비효율적임
하나의
db / 하나의
application이기때문
분리
가되면 db도, 모듈도 분리가되어버린다.
case 2 read only 로 설정방법 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 @Table(name = "suppliment") @Entity ... public class Suppliment { @Id private long suppliment; @Column(name="market_id") private long marketId; @Column(name="factory_id") private long factoryId; @ManyToOne(cascade=CascadeType.ALL) @JoinColumn(name="market_id") private Market marketId; @ManyToOne(cascade=CascadeType.ALL) @JoinColumn(name="factory_id") private Factory factoryId; @ManyToOne @JoinColumn(name="market_id", updatable = false, insertable = false) private Market market; @ManyToOne @JoinColumn(name="factory_id", updatable = false, insertable = false) private Factory factory; }
더 작은 서비스로..
root 에그리거트를, module로 분리하게되면?
applicaton의 기술스택 뿐 아니라 db도 분리가가능
db 분리시 기존에 사용하던 db말고 다른 플랫폼을 사용 할 수 있다
실제로 insert/update 용 DB와 select용 DB를 따로 쓰도록 설계하기도한다.(CQRS)
@OnetoOne 의 optional=false와, @Column의 nullable=falses
repository
Market 도메인을 위한 repository를 만들어보자.
JpaRepository<Entity
, Type of EntityKey
> 를 extends 한 interface를 만든다.
1 2 3 4 5 6 7 8 public interface MarketRepository extends JpaRepository <Market, Long> { Market findByMarketId (Long marketId) ; }
select 1 2 3 4 5 6 7 public Collection<Market> findAll () { Collection<Market> allMarket = marketRepository.findAll(); } public Collection<Market> findById (Long key) { Market market = marketRepository.findByMarketId(key); }
insert 1 2 3 public void insert (Market market) { marketRepository.save(market); }
-> 1:n에서 불필요하게 query가 많이 날아가는경우가있다. (N+1 문제라고한다. 뒤에 설명)
update save가 같은역할을 함
1 2 3 public void update (Market market) { marketRepository.save(market) }
N+1 문제 ★★★★ N+1 문제란? 부모객체의 데이터를 가지고오는 경우 select
쿼리 호출 횟수가 기하급수로 늘어나는현상. 부모데이터 select 1번 + 자식 데이터 수만큼 select 성능에 막대한 영향을 미침.
Q 아래와 같이 데이터가 쌓인경우, query 호출횟수는?
market - 10 row
address - 10 row
department - 각각 10row
car - 각 10row
building - 각 10 row
답 :10 + 10x(department) = 10 + 10x(10+ 10x(cars + building)) = 10 + 10x(10+ 10x(10 + 10)) = 2110
–> 한번에 join을 걸어서 가지고오면 좋을탠데..
jpql
Java Persistence Query Language
JPQL은 SQL과 비슷한 문법을 가진 객체 지향 쿼리
String jpql = "select c from Category c ";
처럼 사용
QueryDSL ★★
원하는대로 join 가능
복잡한쿼리 가능
custom하게 where절 만들기 가능
(매우매우 단순한 applicatoin 이 아니라면 쓰게 custom 한 쿼리를 짜게됨…)
jpql에 비해 얻을수 있는점
IDE의 코드 자동 완성 기능 사용
문법적으로 잘못된 쿼리를 허용하지 않음
도메인 타입과 property를 안전하게 참조할 수 있음
더 많은 타이핑이필요
예를들면 아래와 같은것들이 가능하다
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public Market find (String, departmentsName, String carName) { return from(market) .innerJoin(market.departments, department ) .fetchJoin() .innerJoin(department.cars, car) .fetchJoin() .where( department.name.eq(departmentsName), car.name.eq(carName) ) }
설정방법
buildscript에 plugin 추가
build.gralde (root)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 buildscript { ext { springBootVersion = '2.0.5.RELEASE' querydslPluginVersion = '1.0.10' } repositories { mavenCentral() jcenter() maven { url "https://plugins.gradle.org/m2/" } } dependencies { classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}" ) classpath("gradle.plugin.com.ewerk.gradle.plugins:querydsl-plugin:${querydslPluginVersion}" ) } }
의존성 추가
build.gradle (module)
1 2 3 4 5 6 7 8 9 10 11 12 dependencies { compile("org.springframework.boot:spring-boot-starter-data-jpa" ) ... compile ("com.querydsl:querydsl-core:${querydslVersion}" ) compile ("com.querydsl:querydsl-apt:${querydslVersion}" ) compile ("com.querydsl:querydsl-jpa:${querydslVersion}" ) ... } apply from: "$projectDir/queryDsl.gradle"
플러그인적용 및 querydsl Task 추가
queryDsl.gradle
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 apply plugin: "com.ewerk.gradle.plugins.querydsl" def querydslSrcDir = 'src/main/generated' querydsl { library = "com.querydsl:querydsl-apt" jpa = true querydslSourcesDir = querydslSrcDir } sourceSets { main { java { srcDirs = ['src/main/java' , querydslSrcDir] } } }
repository에 적용 생성된 테스크를 수행하면 generated
폴더에 QClass가 생성됨
QClass란?
@Entity 가 붙은 class들을 찾아 자동으로 생성함
EntityPathBase 과같이 `EntityPathBse 를 상속함
entity 의 구성요소 등을 파악하여 쿼리를 생성할수있게 도아줌
query dls 은 해당 class를 기반으로 table -> from / member variable -> columns 로 매칭하여 쿼리를 만들수 있게 도아줌
적용방법 MarketRepository.java
1 2 3 4 5 6 7 8 public interface MarketRepository extends JpaRepository <Market, Long> , MarketRepositoryCustom { Market findByMarketId (Long marketId) ; }
MarketRepositoryCustom.java
1 2 3 public interface MarketRepositoryCustom { Market findWithoutBuildingByDepartmentNameAndCarName (String DepartmentName, String CarName) ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class MarketRepositoryCustomImpl extends QuerydslRepositorySupport implements MarketRepositoryCustom { QMarket market = QMarket.market; QDepartment department = QDepartment.department; QCar car = Qcar.car; public Market findWithoutBuildingByDepartmentNameAndCarName (String DepartmentName, String CarName) { return from(market) .innerJoin(market.departments, department ) .fetchJoin() .innerJoin(department.cars, car) .fetchJoin() .distinct() .where( department.name.eq(departmentsName), car.name.eq(carName) ) } }
MarketService.java
1 2 3 public getSomething () { marketRepository.findWithoutBuildingByDepartmentNameAndCarName("payco" , "bungbung" ); }
구조 1 2 3 4 5 6 7 8 9 10 11 12 interface MarketRepository interface MarketRepositoryCustom interface JpaRepository class MarketRepositoryCustomImpl class MarketService MarketRepository <|- JpaRepository : extends MarketRepository <|- MarketRepositoryCustom : extends MarketRepositoryCustom --> MarketRepositoryCustomImpl : implments MarketService <-- MarketRepository : DI
quertdsl where절 활용하기 where절 조회 파라메터가 empty 값일때 무시하기 1
null check 후 eq를 비교한다1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public Market findWithoutBuildingByDepartmentNameAndCarName (String DepartmentName, String CarName) { return from(market) .innerJoin(market.departments, department ) .fetchJoin() .innerJoin(department.cars, car) .fetchJoin() .distinct() .where( equalsIfNotNull( department.name, departmentsName), equalsIfNotNull(car.name, carName) car.name.eq(carName) ) } private BooleanExpression equalsIfNotNull (StringPath column, String param) { return param== null ? null : column.eq(param); } private BooleanExpression equalsIfNotEquals (StringPath column, String param) { return StringUtils.isEmpty(param) ? null : column.eq(param); } ....
단점 : string 같은경우 empty check를해야함
날짜의 경우 before/after를 비교하고싶음
….
where절 조회 파라메터가 empty 값일때 무시하기 2 1 2 3 4 5 6 7 8 9 10 11 12 13 public Market findWithoutBuildingByDepartmentNameAndCarName (String DepartmentName, String CarName) { return from(market) .innerJoin(market.departments, department ) .fetchJoin() .innerJoin(department.cars, car) .fetchJoin() .distinct() .where( WhereClauseBuilder.optionalAnd( department.name, department.name.eq(departmentsName)), WhereClauseBuilder.optionalAnd( car.name, car.name.eq(carName)) ) }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class WhereClauseBuilder { public static <T> BooleanExpression optionalAnd(T param, BooleanExpression booleanExpression) { if (param instanceof String) { String stringParam = (String) param; if (StringUtils.isEmpty(stringParam)) { return null; } else { return booleanExpression; } } if (param != null) { return booleanExpression; } return booleanExpression } }
where절 조회 파라메터가 empty 값일때 무시하기 3
1 2 3 4 5 6 7 8 9 10 11 12 public Market findWithoutBuildingByDepartmentNameAndCarName (String DepartmentName, String CarName) { return from(market) .innerJoin(market.departments, department ) .fetchJoin() .innerJoin(department.cars, car) .fetchJoin() .distinct() .where( WhereClauseBuilder.optionalAnd( department.name, ()->department.name.eq(departmentsName)), WhereClauseBuilder.optionalAnd( car.name, ()->car.name.eq(carName)) ) }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class WhereClauseBuilder { public static <T> BooleanExpression optionalAnd (T param, LazyBooleanExpression booleanExpression) { if (param instanceof String) { String stringParam = (String) param; if (StringUtils.isEmpty(stringParam)) { return null ; } else { return booleanExpression.get(); } } if (param != null ) { return booleanExpression.get(); } return null ; } }
1 2 3 4 5 @FunctionalInterface public interface LazyBooleanExpression { BooleanExpression get () ; }
where절 조회 특정 조건일때 무시하기 3 1 public static BooleanExpression ignoreAnd (boolean isTrue, LazyBooleanExpression booleanExpression)
one to one에서 방향
기존 mysql rdb 기반에서의 구조
child가 자신의 id를 가지고, 부모의 키를 fk로 가진다
one to one 관계에서 lazy loading이 불가능해진다.
JPA에서 추구하는 방향
관계의 맺고 끊음 주체가 되는 도메인, 즉 부모측에서 key를 관리
한다.
매핑이 되는 key를 공통으로 사용하더라도 이슈가 없다.
eticket에서는?
tips boolean 1 2 3 @Type(type = "yes_no") @Column(name = "pin_search_success_yn") private boolean isSucceeded;
@Type(type = “yes_no”) 붙여주면 db값의 y,n 을 true/false로 바꿔주어 boolean으로 사용가능하다