728x90

#타임리프 레이아웃 

코드의 재사용이 가능한 부분을 템플릿화할 수 있게 도와주는 타임리프 라이브러리 

 

#레이아웃을 사용하는 이유 

타임리프의 insert나 replace 기능은 많이 사용하지만 이 기능에는 한가지 문제점이 있다. 

 

대부분의 사이트는 탑, 바텀, 네이게이션바 등을 고정적으로 사용한다. 

그래서 매 페이지마다 top(header), bottom, navigationBar를 insert 해주게된다. 

insert를 사용하면 탑에 내용이 바뀐다면 탑만 수정해주면 모든 페이지의 탑이 바뀌게 된다. 

 

그런데 만약 insert된 코드의 내용이 아니라 insert된 코드, 다른 코드들 간의 구조에 변화가 생긴다면 

모든 페이지의 fragment들의 구조를 바꿔줘야 한다. 

이런 한계를 커버해 줄 수 있는 기능이 layout이다. 

기본적인 레이아웃의 사용법을 이미지화한 모습이다.

 

공통요소는 레이아웃에 담아두고 바뀌는 부분만 각각의 페이지에 작성하면 된다. 

 

product.html 페이지로 forwarding, redirect되면 product.html 페이지의 코드들이 레이아웃에 merge되어 렌더링된다.

 

#Thymeleaf layout을 사용하기 위한 setting

 

gradle 추가

implementation group: 'nz.net.ultraq.thymeleaf', name: 'thymeleaf-layout-dialect', version: '3.1.0'

 

 

layout.html

<!DOCTYPE html>

<html xmlns:th="http://www.thymeleaf.org"

xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">

 

layout을 사용할 파일

<!DOCTYPE html>

<html xmlns:th="http://www.thymeleaf.org"

xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"

layout:decorate="layout/layout(title='Recently Viewed')">

<head>

여기서 layout/layout은 사용할 layout의 root이다. 

즉, resources패키지의 layout 패키지 안의 layout.html 파일이라는 뜻이다. 

(title='Recently Viewed')는 부가적인 기능으로 필요하지 않으면 작성하지 않아도 되고 layout의 root만 작성해줘도 된다.

 

 

#layout의 기본적인 사용

 

layout.html

<th:block layout:fragment="content" />

 

layout을 사용할 파일

 

<th:block layout:fragment="content">

<!-- Page Content-->

<div class="container padding-bottom-3x mb-2">

<div class="row">

<!--profile component-->

<div class="col-lg-4" th:insert="~{mypage_component :: mypage(accountPage='recentview')}"></div>

<!--profile component 끝-->

<div class="col-lg-8">

<div class="padding-top-2x mt-2 hidden-lg-up"></div>

<div class="table-responsive wishlist-table margin-bottom-none">

<!-- recentView Table-->

<!-- 하위 태그들은 size==1이면 빈상자 이미지로 대체-->

<table class="table"

th:insert="~{myProduct-component :: product-table(from='recentview')}"></table>

<!-- recentView Table-->

 

</div>

<hr class="mb-4">

</div>

</div>

</div>

 

</th:block>

layout.html에 원하는 위치에 layout:fragment="이름" 속성을 지정해주고

그 위치에 들어갈 코드를 다른 파일에서 

layout:fragment="이름" 속성을 가진 태그의 안에 작성해주면 된다. 

 

*layout은 head body 모두 작성할것이기 때문에 당연히 html 파일의 구조를 제대로 작성할텐데

layout을 사용할 파일은 head가 필요하지 않을 수도 있다. 

하지만 모든 구조를 갖춰서 작성해주는 것이  좋고, 사용하지 않는 fragment들이라도 작성해주는 것이 좋다. 

 

 

예를들어 layout에 fragment로 navigation, contents, recommendation이 있다면 

layout을 사용하는 product.html에 recommendation fragment를 사용하지 않아도 빈 fragment를 작성해준다면 fragment가 아닌 부분의 코드 위치를 잡는데도 도움이 될 것이다. 

 

#layout head merge

레이아웃의 <head> 는 모든 페이지에서 공통으로 사용할 코드를 작성하면 된다. 

다른 페이지에서 그 페이지에서만 사용할 css나 js가 있다면 그 페이지의 head에 작성하면 

그 페이지에 작성된 head만 적용되는 것이 아니라 layout의 head와 merge되어 같이 적용된다. 

 

#layout 변수 초기화 및 사용 

 

<!DOCTYPE html>

<html lang="ko" xmlns:th="http://www.thymeleaf.org">

<head></head>

<body>

<!-- content start -->

<div class="page-title" th:fragment="page-title(page_title)">

<div class="container">

<div class="column">

<h1 th:text="${page_title}">title</h1>

</div>

<div class="column">

<ul class="breadcrumbs">

<li><a href="index">Home</a></li>

<li class="separator">&nbsp;</li>

<li th:text="${page_title}">title</li>

</ul>

</div>

</div>

</div>

<!-- content end -->

</body>

</html>

레이아웃에 변수를 선언하고 레이아웃을 사용하는 각 페이지마다 그 변수를 초기화하여 사용할 수 있다.

 

기존 타임리프에는 fragment명에 (변수 선언)하면 이후에 ${변수명}으로 사용할 수 있다. 

 

레이아웃에서는 

<th:block th:if="${title!=''}">

<div class="page-title"

th:insert="~{fragment/page-title::page-title(${title})}"

th:remove="tag"></div>

</th:block>

layout.html

 

<html xmlns:th="http://www.thymeleaf.org"

xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"

layout:decorate="layout/layout(title='Recently Viewed')">

recentview.html

레이아웃을 사용하는 파일에서 (변수명='value')라고 하면

layout.html의 ${title}에 value값이 대입된다. 

'Java > Spring Boot' 카테고리의 다른 글

JPA Object Relation Mapping  (0) 2023.10.11
JPA 계층형 게시판  (0) 2023.10.10
Spring Data JPA begins  (0) 2023.10.06
Spring CRUD with RestAPI  (0) 2023.09.20
Spring addViewControllers  (0) 2023.09.14
728x90

Entity 공통으로 포함할 createdAt, updatedAt 필드를 

BaseEntity를 만들어 다른 Entity들이 상속할 수 있게 SuperClass로 사용

@Data

@MappedSuperclass

public class BaseEntity {

@CreationTimestamp

@Column(updatable = false)

private LocalDateTime createdAt;

@UpdateTimestamp

private LocalDateTime updatedAt;

}

entity 관리를 위해 entity 생성, 수정 시간을 기록하는 createAt, updatedAt을 가진다. 

ToString, HashCode, Equals를 포함하는 Data 어노테이션과 

BaseEntity 테이블을 따로 만들지 않고 superclass로 하여 자식 클래스들이 필드만 사용할 수 있게 하기 위해 

@MappedSuperclass 어노테이션을 사용한다. 

 

@Data

@EqualsAndHashCode(callSuper = true)

@ToString(callSuper=true)

@AllArgsConstructor

@NoArgsConstructor

@Builder

@Entity

public class ProductDetail extends BaseEntity {

ProductDetail 엔터티는 BaseEntity를 상속하고 

superclass(BaseEntity)의 @Data 어노테이션(Equals,HashCode,ToString)을 사용하기위해 

@EqualsAndHashCode,ToString 어노테이션에 callSuper 속성을 true로 한다. 

 

 

product와 productDetail은 1:1 관계이고 

ProductDetail이 Product_id를 FK로 갖고 있으므로 OwnerTable이라 할 수 있다. 

 

public class Product extends BaseEntity {

@Id

@GeneratedValue(strategy = GenerationType.SEQUENCE)

private Long productId;

 

private String name;

private Integer price;

private Integer stock;

 

 

//1:1 (Owner table 아님)

@OneToOne(mappedBy = "product", cascade = CascadeType.PERSIST)

private ProductDetail productDetail;

 

Product는 ProductDetail 객체를 가지고 

1:1 맵핑이므로 @OneToOne 어노테이션을 붙인다. 

 

OwnerTable이 아닌 클래스에서 OwnerTable 의 클래스를 mapping할 때 속성으로 

mappedBy를 작성해줘야 한다. 

Product가 가지고 있는 ProductDetail은 Product에 의해 mapping 되므로 mappedBy product 

 

cascade는 product를 작업할때 product가 가진 productDetail도 함께 작업할지에 대한 옵션이다. 

all, detach, merge, persist, refresh, remove가 있고 persist는 영속성에 추가할 때 관련 엔터티도 함께 추가된다. 

 

public class ProductDetail extends BaseEntity {

@Id

@GeneratedValue(strategy = GenerationType.SEQUENCE)

private Long productDetailId;

private String description;

 

/*

* 1:1 OWNER TABLE 참조키를 갖고 있어서

*/

@OneToOne(cascade = CascadeType.PERSIST)//save할 때 연관된 프로덕트도 같이 하겠다.

@JoinColumn(name="product_id")

@ToString.Exclude

private Product product;//tostring시 product의 detail의 product의 detail 순환참조 stackoverflow

 

 

ProductDetail은 Product를 FK로 가지고 있으므로 OwnerTable이라고 할 수 있다. 

마찬가지로 @OneToOne 어노테이션을 사용하고 cascade 설정을 줄 수 있다. 

오너테이블에서 FK 엔터티를 맵핑할때는 mappedBy 속성이 필요없다. 

대신 @JoinColumn 어노테이션을 작성해줄 수 있다. (default로 해주긴함)

JoinColumn 은 default로 name="FK" 를 가진다. 

JoinColumn의 이름은 productDetail 테이블에서 product를 컬럼으로 가져올때 가질 물리적 이름이다. 

productDetail 테이블에서 Product엔터티는 product_id 라는 FK로 가져가기 때문에 name="product_id"

관례상 보통 모든 엔터티의 PK(Id)는 [entityName]Id 가 아닌 "Id"로 동일하게 주더라도 

JPA에서 productDetail에 product는 product_id로 설정해준다. 

 

만약 ProductDetail에서 Product를, 혹은 Product에서 ProductDetail를 참조하여 ToString을 호출하면 

Product의 ToString은 product가 가진 ProductDetail을 ToString하다가 또 Product를 만나 Product를 ToString 하고 

순환참조하다 stackOverflow에 빠질 것이다. 

이를 막기 위해서 

@ToString.Exclude 어노테이션을 하면 ProductDetail에서 ToString 하는 과정에서 Product를 제외하고 ToString 하기 때문에 순환참조를 막을 수 있다. 

 

ProductDetail Save() 

ProductDetail(Owner Table)에서 save 할때 

productDetail이 가지는 product를 set 해주면 

알아서 product에도 product가 가지는 productDetail을 set해준다. 

Product product = Product.builder().name("name").price(1111).stock(111).build();

ProductDetail detail = ProductDetail.builder().description("des").build();

//연관관계설정

detail.setProduct(product);

productDetailRepository.save(detail);

 

>>> ProductDetail->ProductProduct(super=BaseEntity(createdAt=2023-10-11T18:01:41.229506, updatedAt=2023-10-11T18:01:41.229506), productId=2, name=name, price=1111, stock=111, productDetail=ProductDetail(super=BaseEntity(createdAt=2023-10-11T18:01:41.247503, updatedAt=2023-10-11T18:01:41.247503), productDetailId=1, description=des))

 

Product테이블 save 결과

 

ProductDetail 테이블 save 결과 

 

productDetail는 product를 가지기 때문에 productDetail에 product를 set 하여야 하고 

product는 그렇지 않아도 상관없다. 

productDetail의 product에 cascade persist가 적용되어 product도 함께 save 되었다. 

 

반면 

void productWithProductDetailSaveAndRead() {

 

ProductDetail productDetail=ProductDetail.builder().description("desc").build();

 

Product product=Product.builder().name("name").price(60000).stock(300).build();

/*

* 연관관계설정(OWNER테이블아닌경우)

* Product-->ProductDetail

*/

product.setProductDetail(productDetail);

productDetail.setProduct(product);

productRepository.save(product);

}

오너테이블이 아닌 product에서 product를 함께 save 하기 위해서는 

product는 productDetail을 set 해야하고 

productDetail은 product를 set 해야한다. 

 

만약 

product.setProductDetail(productDetail);

// productDetail.setProduct(product);

productRepository.save(product);

productDetail에 product를 set하지 않으면 

productDetail에 product_id가 null로 된 것을 확인할 수 있다. 

 

Provider와 Product는 1:N 관계이다. 

//1:n(owner table 아님)

@Builder.Default//초기값 주기

@OneToMany(mappedBy = "provider",cascade = CascadeType.PERSIST,fetch = FetchType.EAGER)

private List<Product> products=new ArrayList<>();

Provider(One) Product(Many)이므로 

Provider의 Product는 컬렉션으로 가지고 OneToMany 어노테이션을 사용한다. 

provider는  ownerTable이 아니므로 mappedBy 속성을 부여한다. 

Builder.Default 값은 

Provider클래스의 List<Product>에 new ArrayList<>() 를 초기값을 추기 위해 붙여주는 어노테이션이다. 

 

fetch type이란

Jpa가 Entity를 조회할 때 연관관계에 있는 객체들을 전부 접근할지, 필요할때만 접근할지에 대한 설정이다. 

default는 최적화를 위해 Lazy이고 

연관 관계에 있는 Entity를 가져오지 않고, getter로 접근하는 경우만 가져온다. 

Eager는 항상 연관관계에 있는 Entity를 모두 가져온다. 

 

 

Product는 Provider를 

//n:1 Owner Table

@ManyToOne(cascade = CascadeType.PERSIST,fetch = FetchType.EAGER)//default : lazy

@JoinColumn(name="provider_id")

@ToString.Exclude

private Provider provider;

 

ManyToOne으로 매핑한다. 

 

 

Product product = Product.builder().name("name").price(10000).stock(100).build();

Product product2 = Product.builder().name("name2").price(20000).stock(200).build();

Product product3 = Product.builder().name("name3").price(30000).stock(300).build();

Provider provider = Provider.builder().name("name").build();

product.setProvider(provider);

product2.setProvider(provider);

product3.setProvider(provider);

List<Product> products = provider.getProducts();

products.add(product);

products.add(product2);

products.add(product3);

provider.setProducts(products);

 

providerRepository.save(provider);

product1,2,3, provider를 생성하고 

product들에 provider를 set하고 

Provider의 Products를 가져온다. 

Products는 초기값으로 new ArrayList<>()를 갖고 있다. 

Provider의 products에 product를 add 하고 setProducts 한 후 provider를 save한다. 

한 transaction 안에 (@Transactional)있다면 products.add(product1,2,3) 만 해주고 provider에 다시 setProducts 하지 않아도 context에서 확인하고 save 처리해준다. 

 

 

반대로 Product에서는 

Provider provider=Provider.builder()

.name("name")

.build();

 

Product product=Product.builder()

.name("name")

.price(10000)

.stock(100)

.build();

/***** 연관설정 Product-->Provider *****/

product.setProvider(provider);

productRepository.save(product);

product에  provider만 set해줘도 된다. 

 

 

category엔터티는 

@Column(unique = true,nullable = false)

private String code;

code 컬럼을 unique로 갖고 있다. 

@Column에 unique, nullable 속성을 추가해준다. 

 

@OneToMany(cascade = {CascadeType.ALL},orphanRemoval = true,mappedBy = "category",fetch = FetchType.EAGER)

@Builder.Default

private List<Product> products=new ArrayList<>();

 

orphanRemoval 속성은 자식 엔터티가 고아가 되면 삭제해주는 속성이다. 

이렇게 하면 

findCategory.getProducts().clear();

카테고리에서 product리스트를 지우면 카테고리에서 프로덕트를 갖지 않게되므로 쫓겨난 프로덕트는 고아가 된다. 

그럼 그 고아엔터티를 삭제해주어 product테이블에서도 삭제된다. 

 

'Java > Spring Boot' 카테고리의 다른 글

Thymeleaf Layout  (1) 2023.11.24
JPA 계층형 게시판  (0) 2023.10.10
Spring Data JPA begins  (0) 2023.10.06
Spring CRUD with RestAPI  (0) 2023.09.20
Spring addViewControllers  (0) 2023.09.14
728x90

public class Board {

@Id

@GeneratedValue(strategy = GenerationType.AUTO)

@SequenceGenerator(sequenceName = "board_boardno_seq", name = "board_boardno_seq")

private Long boardno;

private String title;

private String writer;

private String content;

@ColumnDefault("sysdate")

@CreationTimestamp

private LocalDateTime regdate;

private Long readcount;

private Long groupno;

private Long step;

private Long depth;

}

@UpdateTimestamp도 있어서 update시 regdate에 time을 넣어줄 수 있지만 

CreationTimestamp와 중복하여 사용할 수 없다. 

사용하려면 updatedate 필드를 만들어서 붙여야 한다. 

 

@ColumnDefault 어노테이션은 jpa에서 insert할 때는 무시한다. 

하지만 sql을 사용할때는 적용되기 때문에 붙여준다. 

 

Repository

 

List<Board> findByStepGreaterThanAndGroupno(Long step,Long groupNo);

List<Board> findByBoardnoGreaterThanEqualOrderByGroupnoDescStepAsc(Long boardNo);

//Page<Board> findByBoardnoGreaterThanEqualOrderByGroupnoDescStepAsc(Long boardNo,Pageable pageable);

 

 

new_save() 새글 작성하기

void new_save() {

Board board1= Board.builder()

.title("게시판101")

.content("내용101")

.writer("101")

.build();

Board savedBoard1 = boardRepository.save(board1);

savedBoard1.setGroupno(savedBoard1.getBoardno());

savedBoard1.setStep(1L);

savedBoard1.setDepth(0L);

savedBoard1.setReadcount(0L);

savedBoard1= boardRepository.save(savedBoard1);

System.out.println(">>>savedBoard1:"+savedBoard1);

 

 

답글 작성

void reply_save() {

Board findBoard100 = boardRepository.findById(100L).get();

 

List<Board> updateBoardList100 =

boardRepository.findByStepGreaterThanAndGroupno(findBoard100.getStep(), findBoard100.getGroupno());

for (Board tempBoard : updateBoardList100) {

tempBoard.setStep(tempBoard.getStep()+1);

}

boardRepository.saveAll(updateBoardList100);

 

Board board101=Board.builder()

.title("게시판타이틀101")

.content("내용101")

.writer("김경호101")

.groupno(findBoard100.getGroupno())

.step(findBoard100.getStep()+1)

.depth(findBoard100.getDepth()+1)

.readcount(0L)

.build();

boardRepository.save(board101);

 

그룹넘버가 같고 step이 더 큰 글(답글을 달려는 원글의 답글들 , 원글이 답글이 될 수도 있다)에 step을 증가시킴 

board101 글을 원글보다 step, depth를 증가시키고 save 

 

void board_select() {

List<Board> boardList = boardRepository.findAll();

for (Board board : boardList) {

System.out.println(board);

}

boardList = boardRepository.findByBoardnoGreaterThanEqualOrderByGroupnoDescStepAsc(0L);

for (Board board : boardList) {

System.out.println(board);

}

 

글 목록 전체 조회 

list를 groupno 내림차순, step 오름차순으로 정렬하여 select 

orderBy 할때 앞에 조건문이 필요하여 BoardnoGreaterThanEqual 추가해주고 0L인자로 준다. 

 

 

페이징 작업

void board_select_page() {

List<Board> boardList=boardRepository.findAll();

System.out.println(">>>boardList:"+boardList.size());

 

int currentPage=14; //현재페이지

int size=7; //페이지당게시물수

 

Pageable pageable=PageRequest.of(currentPage-1,

size,

Sort.by("groupno").descending()

.and(Sort.by("step").ascending())

);

/*

PageRequest.of(page, size)

- page zero-based page index.(요청페이지)

- size the size of the page to be returned.(페이지당 게시물 수)

*/

Page<Board> page = boardRepository.findAll(pageable);

for (Board board : page.getContent()) {

System.out.println(board);

}

}

Pageable 인터페이스

 

  1. pageNumber: 페이지 번호를 나타냅니다. 0부터 시작하며 첫 번째 페이지는 0입니다. 이 속성은 몇 번째 페이지를 가져올지를 결정합니다.
  2. pageSize: 한 페이지에 표시될 항목 수를 나타냅니다. 예를 들어, 페이지당 10개의 항목을 표시하려면 pageSize를 10으로 설정합니다.
  3. offset: 페이지의 시작 항목의 오프셋입니다. 이것은 pageNumber * pageSize의 값과 같습니다. 데이터베이스 쿼리를 작성할 때 특정 페이지의 데이터를 가져오는 데 사용됩니다.
  4. sort: 페이지의 항목을 정렬하는 데 사용됩니다. Sort 타입의 객체로 정렬 방법과 정렬할 필드를 지정할 수 있습니다. 다음은 Sort를 사용하는 예입니다.이것은 "fieldName" 필드를 오름차순으로 정렬하는 방법을 나타냅니다.
    Sort sort = Sort.by(Sort.Order.asc("fieldName"));
  5. unpaged(): 페이지네이션을 사용하지 않고 모든 데이터를 한 번에 가져오도록 지정합니다. 주로 모든 데이터를 가져올 때 사용됩니다.
  6. next(): 다음 페이지로 이동하는 Pageable 객체를 생성합니다.
  7. previousOrFirst(): 이전 페이지로 이동하거나 첫 번째 페이지로 이동하는 Pageable 객체를 생성합니다.
  8. first(): 첫 번째 페이지로 이동하는 Pageable 객체를 생성합니다.
  9. hasNext(): 다음 페이지가 있는지 확인합니다.
  10. getSort(): 현재 페이지에 적용된 정렬 정보를 가져옵니다.

 

 

Pageable 인터페이스는 데이터베이스 조회 결과를 정렬하고 페이지를 나누고 그 페이징 관련 정보를 담고 있다. 

Pageable pageable=PageRequest.of(currentPage-1,

size,

Sort.by("groupno").descending()

.and(Sort.by("step").ascending())

);

PageRequest의 of (page,size)메서드를 사용하면 페이지를 전체 리스트를 size로 나누어 페이징했을 때 구한 page의 paging 정보를 Pageable 인터페이스로 반환해준다.

 

추가로 page,size, Sort(정렬)을 대입할 수도 있다. 

Sort.by("groupno").descending().and(Sort.by("step").ascending())

 

 

findAll 메서드에 Pageable을 인자로 대입하면 해당 Pageable의 Page를 반환해주고 

Page의 getContent 하여 리스트를 얻을 수 있다. 

반대로 Page에서 getPageable 을 하면 Pageable을 얻을 수 있다. 

 

 

  1. getContent(): 페이지에 포함된 데이터 항목들을 반환합니다. 이 메서드를 통해 현재 페이지의 데이터를 가져올 수 있습니다.
  2. getNumber(): 현재 페이지 번호를 반환합니다. 페이지 번호는 0부터 시작합니다.
  3. getSize(): 페이지 크기(한 페이지 당 항목 수)를 반환합니다.
  4. getTotalPages(): 전체 페이지 수를 반환합니다.
  5. getTotalElements(): 전체 항목 수를 반환합니다. 이는 모든 페이지를 합친 총 데이터 항목 수입니다.
  6. hasContent(): 페이지에 데이터가 포함되어 있는지 여부를 확인합니다.
  7. hasNext(): 다음 페이지가 있는지 여부를 확인합니다.
  8. hasPrevious(): 이전 페이지가 있는지 여부를 확인합니다.
  9. isFirst(): 현재 페이지가 첫 번째 페이지인지 여부를 확인합니다.
  10. isLast(): 현재 페이지가 마지막 페이지인지 여부를 확인합니다.
  11. nextPageable(): 다음 페이지로 이동하는 데 필요한 Pageable 객체를 반환합니다.
  12. previousPageable(): 이전 페이지로 이동하는 데 필요한 Pageable 객체를 반환합니다.

'Java > Spring Boot' 카테고리의 다른 글

Thymeleaf Layout  (1) 2023.11.24
JPA Object Relation Mapping  (0) 2023.10.11
Spring Data JPA begins  (0) 2023.10.06
Spring CRUD with RestAPI  (0) 2023.09.20
Spring addViewControllers  (0) 2023.09.14
728x90

http://projects.spring.io/spring-data/

 

Spring Data

Spring Data’s mission is to provide a familiar and consistent, Spring-based programming model for data access while still retaining the special traits of the underlying data store. It makes it easy to use data access technologies, relational and non-rela

spring.io

 

 

ORM object relation mapping

 

ORM은 자바 객체와 SQL DB의 테이블 사이의 맵핑 메타데이터를 사용하여

자바 객체를 SQL DB의 테이블에 자동으로 영속화해주는 기술 

 

객체를 릴레이션 테이블에 맵핑할 때 생기는 문제

- 밀도 granularity 문제 : 객체는 다양한 크기의 다양한 커스텀 객체를 만들기 쉬우나 릴레이션은 기본 타입만 사용할 수 있다. 

- 서브타입 subtype 문제 : 객체는 상속 구조를 만들기 쉽지만 릴레이션은 그렇지 못한다

- 식별성 identity 문제 : 객체를 동일한 객체로 인식할때 레퍼런스가 같은면 동일하다고 할지, 레퍼런스가 달라도 가지고 있는 멤버와 그 값이 동일할 때 같다고 할지에 대해서 정해줘야 하지만 릴레이션에서는 주키를 같으면 동일한 인스턴스로 본다.

- 관계 association 문제 : 객체는 방향성이 존재하고 다대다 맵핑이 가능하지만 릴레이션은 방향성이 존재하지 않고 외래키로 관계를 표현할 뿐이다.

- 데이터 네비게이션 navigation 문제 : 객체는 콜렉션을 순회하고 레퍼런스를 이용해 다른 객체로 이동도 가능하지만

릴레이션은 데이터베이스에 성능을 위해 적은 요청으로 여러 릴레이션에 접근하기 위해 join을 사용해야한다. 

 

스프링 데이터 

 

- 스프링 데이터 : SQL, NoSQL 저장소 지원

- 스프링 데이터 common : 여러 저장소 지원 프로젝트의 공통 기능 ( Repository > CrudRepository > PagingAndSortingRepository )

- 스프링 데이터 rest : 저장소의 데이터를 하이퍼미디어 기반 HTTP리소스로 제공

- 스프링 데이터 JPA : 스프링데이터 common이 제공하는 기능에 JPA 관련 기능 추가 ( JpaRepository )

 

 

 

 

 

 

 

 

JPA 실습

 

new Spring Starter Project 

DI 

spring data JPA

lombok

spring boot devTools

oracle JDBC

(JDBC API)

 

application.properties

Generic Editor로 열면 content assist 사용가능 

 

spring.datasource.url=jdbc:oracle:thin:@111.111.111.11:1521:xe

spring.datasource.username=j

spring.datasource.password=j

#테이블 자동 생성 옵션

spring.jpa.hibernate.ddl-auto=create

JPA는 구현체로 Hibernate를 사용한다.

 

#시작시 drop테이블시 error가 발생하면 멈춤

spring.sql.init.continue-on-error=false

 ddl create 작업 실행시 drop 후 create를 하는데 최초에는 table이 없기 때문에 drop시 오류가 발생하고 실행을 멈추는데 

실행이 멈추는 걸 막고 계속 실행하게 하는 설정 

 

#JPA sql 초기화 작업 user-insert.sql을 테이블이 drop-create 이후 자동으로 실행시켜줌

spring.sql.init.mode=always

spring.sql.init.data-locations=classpath:user-insert.sql,classpath:guest-insert.sql

#spring.sql.init.schema-locations=classpath:user.ddl

spring.sql.init 은 sql이 시작될때 바로 실행되는 작업을 뜻한다. 

나중에 테스트를 위해 sql insert 작업을 할 때 

insert.sql 파일로 다량의 sql 작업을 처리할 때 data-locations에 insert.sql 파일을 알려준다. 

sql 시작할때 테이블을 생성할 ddl 파일을 init으로 실행하기 위한 설정으로 schema-locations에 ddl 파일의 위치를 기술 

여기서는 hibernate.ddl-auto=true 설정을 하여 생략하였다.

 

spring.jpa.defer-datasource-initialization=truetrue로 설정해서 hibernate 초기화 이후 data.sql가 실행되도록 변경한다.

 hibernate 초기화 이후 sql이 실행되도록 지연시키는 설정 

 

spring 실행 로그

#SQL을 로그로 보여주는 옵션, 상세보기 옵션

spring.jpa.show-sql=true

spring.jpa.properties.hibernate.format_sql=true

 

logging.level.org.hibernate.orm.jdbc.bind=trace

쿼리에 binding 하는 파라메터를 보여줌 

 

 

 

 

 

 

 

 

entity 패키지 하위 UserEntity 클래스 생성

@Entity(name="user")

entity는 객체세상에서 table을 표현하는 이름으로 entity의 이름은 default로 클래스의 이름(소문자)로 설정되고 name property로 지정해 줄 수 있다. 

@Table(name="userinfo")

table은 Relation에서의 table 이름으로 대부분의 DB에서는 user를 예약어로 사용하므로 user를 사용하지 못한다.

 

@Id

private String userid;

PK 컬럼으로 할 멤버필드에는 @Id 어노테이션을 붙인다.

 

@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "guest_guest_no_seq")

@SequenceGenerator(name = "guest_guest_no_seq", sequenceName = "guest_guest_no_seq", initialValue = 1, allocationSize = 1)

@GeneratedValue() 시퀀스 같이 데이터를 자동으로 생성해준다. 

@SequenceGenerator(name="", sequenceName="", ...) 

sequenceName은 실제로 생성되는 물리적인 seq의 이름이고 name은 개발자가 sequence에 붙여준 이름으로 

GeneratedValue에 generator 이름은 @Generator 의 name을 입력한다. 

 

 

@Entity(name="user")

@Table(name="userinfo")

@Data

@AllArgsConstructor

@NoArgsConstructor

@Builder

public class User {

@Id

private String userId;

private String password;

private String name;

private String email;

}

 

이렇게 설정하고 실행시키면 userinfo 테이블이 생성된다. 

#테이블 자동 생성 옵션

spring.jpa.hibernate.ddl-auto=create

userId 카멜케이스로 하면 JPA는 테이블에 스네이크케이스로 바꿔준다.

 

private String userid;

private String password;

private String name;

private String email;

userid로 하면 테이블에 

userid 로 컬럼명이 설정된다. 

 

컬럼명을 명시해주고 싶을 땐 @Column(name="") 어노테이션을 사용

@Column(name="useridTest")

@Id

private String userid;

 

 

 

++

private String user_id;

만약 user_id라고 스네이크 케이스로 객체의 멤버변수명을 설정하면 

나중에 JPA에서 쿼리를 날릴때 문제가 생길 수 있으므로 사용을 지양한다.

 

 

 

@Column 어노테이션을 컬럼에 length, nuallable, constraints 등을 설정할 수 있다. 

@Column(length = 10, nullable = false)

 

@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "guest_guest_no_seq")

@SequenceGenerator(name = "guest_guest_no_seq", sequenceName = "guest_guest_no_seq", initialValue = 1, allocationSize = 1)

@Id

private Long guestNo;//wrapper객체로 쓰는게 좋고 Integer보단 long

 

JPA를 사용할 땐 가급적 기본형보단 WrapperClass를 사용해주는게 좋은데 

자릿수가 짧은 Integer보단 Long을 사용하는 것이 좋다.

 

@ColumnDefault("sysdate")

private Date guestDate;

컬럼에 default 값을 설정해줄때는 

@ColumnDefault 어노테이션을 사용한다. 

 

또는 Column에 columnDefinition에 ddl에 작성하는 columnDefinition부분을 작성하면 된다. 

DATE DEFAULT SYSDATE

@Column(columnDefinition = "date default sysdate")

private Date guestDate;

 

 

repository 인터페이스 생성 

 

Jpa를 사용하기 위해  JpaRepository<entity,idtype>  상속extends 

JpaRepository는 CrudRepository를 상속하고 

CrudRepository는 

<S extends T> S save(S entity);

<S extends T> Iterable<S> saveAll(Iterable<S> entities);

Optional<T> findById(ID id);

boolean existsById(ID id);

...

등 비즈니스에 필요한 다양한 메소드들이 있다. 

 

Repository test하기 

 

 

repository에서 new- JUnit Test Case 생성

TestCase를 생성하면 Repository에서 상속한 인터페이스와 그 상위 인터페이스들의 메소드들을 테스트 할 수 있다. 

테스트할 클래스에 @SpringBootTest 어노테이션을 붙여주거나 

SpringJpaApplicationTests를 상속받아 SpringJpaApplicationTests의 @SpringBootTest도 상속받아 쓴다. 

@DisplayName("회원아이디로 찾기")

@Test

@Disabled

void testFindById() {

Optional<User> optionalUser = userRepository.findById("guard1");

// optional객체는 null일 수 없다.

if (optionalUser.isPresent()) {

User findUser1 = optionalUser.get();

System.out.println(findUser1);

}

}

 

 

 

JPA에서 기본 쿼리가 아닌 쿼리가 필요할 때에도 SQL문을 작성할 필요가 없다.(물론 직접 작성해줄 수도 있다.)

 

JPA는 method Interceptor로 메소드의 호출시 method를 가로채 메소드의 이름으로 필요한 쿼리를 작성해 구현해준다. 

그래서 우리가 어떤 쿼리가 필요하면 그에 맞게 이름을 지어줘야한다. 

https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods.query-creation

 

Spring Data JPA - Reference Documentation

Example 121. Using @Transactional at query methods @Transactional(readOnly = true) interface UserRepository extends JpaRepository { List findByLastname(String lastname); @Modifying @Transactional @Query("delete from User u where u.active = false") void del

docs.spring.io

메소드 이름 짓는 방법은 위 문서에 나와있다. 

 

List<User> findByName(String name);

List<User> findByNameAndEmail(String name,String email);

List<User> findFirst2ByName(String name);

List<User> findTop2ByName(String name);

List<User> findByNameContains(String name);

List<User> findByNameStartingWith(String name);

List<User> findByNameLike(String name);

System.out.println(">>>findByNameLike :"+userRepository.findByNameLike("%경호%").size());

리턴 타입으로는 

void, 기본형, Wrapper, T(Object), Optional<T>, List<T>, Stream<T>, Iterator<T>, Collection<T>, Page<T>, Slice<T> ...

메소드의 이름은

find, get, search, read, stream, query, count... 으로 시작하고 

find(_찾을객체_)By_조건 형식으로 한다.

 

@Query 어노테이션 : 쿼리를 직접 작성할 때 

@Query(value="select * from userinfo where name=?1",nativeQuery = true)

List<User> findByNameSQL(String name);

nativeQuery는 JPQL이 아닌 SQL문을 그대로 작성할 때 사용한다.

 

select * from userinfo where name=?1에서 

?1은 매개변수 순서를 나타낸다. 

?1 자리에 첫번째 매개변수인 String name이 들어간다는 뜻이다. 

 

@Test

@DisplayName("방명록저장")

void save() {

Guest guest = Guest.builder().guestName("name").guestEmail("email").guestHomepage("home").guestContent("content").guestTitle("title").build();

System.out.println(guestRepository.save(guest));

System.out.println(guest);

}

guest와 save한 후 반환되는 guest는 같은 객체이다 

guest에 save해서 pk가 생성되었을때 guest에도 pk가 set된다. 

*@ColumnDefault("sysdate")로 디폴트 값을 설정해주었는데 guestDate=null 값이다..

 

hibernate에서 제공하는 @CreationTimestamp 어노테이션을 사용하면 된다. 

@Column(columnDefinition = "date default sysdate")

@CreationTimestamp// insert시 자동으로 값을 채워줌

private Date guestDate;

 

@Test

@DisplayName("방명록수정")

void update() {

Guest guest = guestRepository.findById(1L).get();

System.out.println("Guest 1L : "+guest);

guest.setGuestContent("내용");

guest.setGuestName("이름");

guestRepository.save(guest);

System.out.println("Guest 1L update: "+guest);

 

}

guest의 pk 타입은 Long 이기 때문에 1에 L을 붙여준다.

 

업데이트

guest update를 하는데 내용과 이름만 바꿨는데 쿼리는 모든것을 수정하고 있다. 

이는 성능에도 영향을 주기 때문에 수정이 필요한부분만 수정하는 쿼리를 만들도록 해준다.

@DynamicUpdate

public class Guest {

guest에 @DynamicUpdate 어노테이션을 추가한다.

 

한번에 여러 인스턴스를 업데이트하는 방법 

List<Guest> list =guestRepository.findByGuestNoBetween(1L, 5L);

for (int i = 0; i < list.size(); i++) {

list.get(i).setGuestName("nameChange");

}

guestRepository.saveAll(list);

 

saveAll()로 list를 모두 save 할 수 있다.

'Java > Spring Boot' 카테고리의 다른 글

JPA Object Relation Mapping  (0) 2023.10.11
JPA 계층형 게시판  (0) 2023.10.10
Spring CRUD with RestAPI  (0) 2023.09.20
Spring addViewControllers  (0) 2023.09.14
Thymeleaf in Spring  (0) 2023.09.14

+ Recent posts