68일차
CRUD 검색 페이징
12.8 file 저장 - 3
글쓸때 일어나야한다.
컨트롤러 서비스 매퍼 view javaBean 수정이 필요하다.
12.8.1 view
글쓰기 폼에 먼저 첨부파일을 넣을 수 있게 수정해준다.
multipart/form-data로 인코딩되게 해줘야한다.
accept attribute를 지정해주면 이미지 파일만 저장되도록 할 수 있다.
<form method="post" enctype="multipart/form-data">
<div class="mb-3">
<label for="titleInput" class="form-label">제목</label>
<input type="text" id="titleInput" class="form-control" name="title" value="${board.title}" />
</div>
<div class="mb-3">
<label for="bodyInput" class="form-label">내용</label>
<textarea rows="10" name="body" id="bodyInput" class="form-control">${board.body}</textarea>
</div>
<div class="mb-3">
<label for="writerInput" class="form-label">작성자</label>
<input type="text" id="writerInput" name="writer" class="form-control" value="${board.writer}" />
</div>
<div class="mb-3">
<input type="file" name="files" accept="image/*" multiple />
</div>
<div class="mb-3">
<input class="btn btn-primary" type="submit" value="등록" />
</div>
</form>12.8.2 컨트롤러
MultipartFile 객체로 파일을 받고 서비스에게 전달해주면된다.
@PostMapping("add")
public String addProcess(
@RequestParam("files") MultipartFile files,
Board board, RedirectAttributes rttr) {
boolean ok = service.create(board, files);
}12.8.3 서비스
파일 저장 (파일 시스템 하드디스크에 저장) 먼저 해보자.
file이 들어왓을때만 일하게 해줘야한다. file size가 0보다 클때만 file관련일을 하게 하면된다.
public boolean create(Board board, MultipartFile[] files) {
for ( MultipartFile file : files) {
if (file.getSize() > 0) {
System.out.println(file.getOriginalFilename());
System.out.println(file.getSize());
}
}
int cnt = mapper.insert(board);
return cnt == 1;
}12.8.4 서비스 - 2
upload파일에 저장을 할 것이다.
파일경로 + 파일이름으로 경로를 얻는다.
public boolean create(Board board, MultipartFile[] files) throws Exception {
for ( MultipartFile file : files) {
if (file.getSize() > 0) {
System.out.println(file.getOriginalFilename());
System.out.println(file.getSize());
//파일 저장 (파일 시스템 하드디스크에 저장)
String path = "F:\\study\\upload\\" + file.getOriginalFilename();
File target = new File(path);
file.transferTo(target);
}
}
int cnt = mapper.insert(board);
return cnt == 1;
}12.8.5 서비스 - 3
여러 게시물을 작성하면 같은 폴더에 모인다.
파일 이름이 같으면 덮어씌우게 되버린다.
이름을 다 다르게 저장이 되도록 하거나.
게시물마다 폴더를 만들어서 분리를 해보자.
게시물의 primary key를 얻고 저장해보자.
"F:\study\upload\" + board.getId() + File.separator + file.getOriginalFilename();으로 저장하면된다.
폴더가 없어서 오류가 발생한다.
그래서 글만 올라가게 할것인지 첨부파일까지 하는것을 하나의 트렌잭션인지를 정해줘야한다.
그래서 다음 두가지 일을 해야한다.
1.게시물번호로 폴더만들기
2.트랜잭션 처리하기
public boolean create(Board board, MultipartFile[] files) throws Exception {
// 게시물 insert
int cnt = mapper.insert(board);
for (MultipartFile file : files) {
if (file.getSize() > 0) {
System.out.println(file.getOriginalFilename());
System.out.println(file.getSize());
// 파일 저장 (파일 시스템 하드디스크에 저장)
//폴더만들기
String folder = "F:\\study\\upload\\" + board.getId();
File targetFolder = new File(folder);
if (!targetFolder.exists()) {
targetFolder.mkdirs();
}
//파일 저장
String path = folder + File.separator + file.getOriginalFilename();
File target = new File(path);
file.transferTo(target);
}
}
// db에 관련정보저장 (insert)
return cnt == 1;
}12.8.6 db저장
Board에 파일 컬럼을 추가하는게 맞나?
하나의 컬럼을 추가하면 파일이 여러개라면 하나의 컬럼에 여러 파일이 들어가게 된다.
그러면 원자적이지 않아서 1정규화에 맞지않다고 생각할 수 있다.
그렇다고 같은 역할을 하는 여러 컬럼을 만드는 것도 제1정규화에 맞지 않는다.
그래서 file관련 테이블을 따로 만들어주는 것이 좋다.
Board의 id와 같이 이어져야하니 외래키설정도 해준다.
CREATE TABLE FileNames (
id INT PRIMARY KEY AUTO_INCREMENT,
boardId INT NOT NULL,
fileName VARCHAR(300) NOT NULL,
FOREIGN KEY (boardId) REFERENCES Board(id)
);mapper에서 이테이블에 insert하는 메소드를 만들어준다.
@Insert("INSERT INTO FileNames (boardId, fileName) "
+ "VALUES (#{id}, #{originalFilename})")
int insertFileName(Integer id, String originalFilename);12.8.7 트랜젝션 처리 - 1
커넥션을 직접 다룰필요없이 @Transactional 어노테이션을 사용하면
스프링이 알아서 트랜젝션 처리를 해준다.
@Update("""
UPDATE Bank
SET money = money - 500
WHERE name = 'A'
""")
void minusA();
@Update("""
UPDATE Bank
SET money = money + 500
WHERE name = 'B'
""")
void plusB();@GetMapping("link3")
@Transactional
public void method3() {
// A고객의 돈 500원 차감
mapper.minusA();
int a = 3 / 0; // runtime exception
// A고객의 돈 500원 증가
mapper.plusB();
}12.8.8 트랜젝션 처리 - 2
runtime exception이아닌 checked exception을 발생시켜보자.
@GetMapping("link4")
@Transactional
public void method4() throws Exception {
// A고객의 돈 500원 차감
mapper.minusA();
Class.forName("java.lang.String2"); //checked exception
// A고객의 돈 500원 증가
mapper.plusB();
}그런데 @Transactional어노테이션은 checked exception이 발생하면 롤백을 시키지 못한다.
runtime exception 즉 에러일때만 rollback이 된다.
rollbackFor()라는 element를 작성해야한다.
어떤 예외에 rollback하겟다를 .class로 지정할 수 있다.
Exception.class으로 하면 모든 예외를 롤백하는 것이다.
@GetMapping("link5")
@Transactional(rollbackFor = Exception.class)
public void method5() throws Exception {
// A고객의 돈 500원 차감
mapper.minusA();
Class.forName("java.lang.String2"); //checked exception
// A고객의 돈 500원 증가
mapper.plusB();
}12.8.9 트랜젝션 처리 - 3
프로젝트에 반영해보자.
@PostMapping("add")
@Transactional(rollbackFor = Exception.class)
public String addProcess(
@RequestParam("files") MultipartFile[] files,
Board board, RedirectAttributes rttr) {
// 새 게시물 db에 추가
try {
boolean ok = service.create(board, files);
if (ok) {
rttr.addFlashAttribute("success", "insertScucess");
rttr.addFlashAttribute("message", "게시물이 등록되었습니다.");
// return "redirect:/list";
return "redirect:/detail/" + board.getId();
} else {
rttr.addFlashAttribute("fail", "insertFail");
rttr.addFlashAttribute("message", "게시물 등록에 실패했습니다. 다시입력해주세요");
rttr.addFlashAttribute("board", board);
}
} catch (Exception e) {
e.printStackTrace();
}
return "redirect:/add";
}12.8.10 하드디스크 파일 보여주기
aws는 그냥 보여주면되는데 하드디스크에 있는 파일을 보여주려면 트릭이 필요하다.
server에 modules를 보면 웹모듈을 설정할 수있다.
add External Web Modeule
F:\study\upload파일을 지정하고 path를 이미지이니 /image로 지정해보자.
그리고 서버를 실행해준다.
http://localhost:8080/image/폴더명/파일명.확장자 하면 인터넷에 서비스 되는 것처럼 보이게 된다.
우리 마지막에 실행한 톰캣이 포트를 8080쓰고 있다.
같은 컴퓨터에서 여러 프로그램이 여러 포트를 쓸수없다.
프로젝트의 포트번호를 바꿔보자.
application.properties에서 설정을 바꾸면된다.
server.port=8081
게시물을 볼때 파일을 열어줘야하니 detail.jsp를 수정해줘야한다.
localhost:8080/image/게시물번호/fileName로 파일을 요청해야한다.
서버를 두개 열어놓어 놓아서 트릭을 사용하는 것이다.
aws에 올리면 앞의 주소가 aws의 주소가 되는 것이다.
<!-- 그림 파일 출력 -->
<div class="mb-3">
<c:forEach items="${fileNameList}" var="fileName">
<div>
<%-- localhost:8080/image/게시물번호/fileName --%>
<img src="localhost:8080/image/${board.id}/${fileName}" alt="" />
</div>
</c:forEach>
</div>컨트롤러에서 fileNameList의 attribute로 file을 받아오면된다.
12.8.11 mapper - 1
JOIN으로 값을 게시글 당 게시물을 정리해줘야한다.
@Select("SELECT b.id, b.title, b.body, b.writer, b.inserted, f.fileName FROM SELECT * FROM Board b "
+ "LEFT JOIN FileName f ON b.id = f.boardId "
+ "WHERE b.id = #{id}")
Board selectById(Integer id);그런데 이렇게 하면 여러 파일이면 board가 여러개가 조회가 되는 게 잇어서 오류가 발생한다.
Join매핑은 조회된 결과를 javaBean에 담을때 어노테이션 지원을 안하고 있다.
XML파일을 사용해보자.
결론은 file이름이 들어가는 javabean에 list로 넣을 것이다.
기존Mapper와 이름이 같게 BoardMapper.xml을 같은 패키지안에 만들어줘야한다.
xml관련된 코드를 작성해야한다. 마이바티스 공식문서에서 다음내용을 추가해줘야한다.
https://mybatis.org/mybatis-3/getting-started.html
Exploring Mapped SQL Statements
namespace에는 이 xml과 연결할 mapper의 full name을 작성하면된다.
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.BoardMapper">
</mapper>12.8.12 mapper - 2
하나의 게시글들이 여러 레코드로 나오는 경우가 있다.
JOIN하지 않앗을때는 레코드가 하나만 나오는데 JOIN하면 여러 레코드가 나오게 된다.
조회결과와 JavaBean과 매핑해주는게 필요하다.
그 element가 resultMap이다.
이안쪽에 result element로 매핑정보를 작성해주면된다.
어떤 컬럼이 어떤 proerty에 저장이 되는지를 지정해주면된다.
컬럼 이름은 쿼리에서 따오는 것이고 property는 Board의 property를 사용하는 것이다.
resultMap의 type에 어떤 entity로 들어가는지를 지정해주면된다.
primary key는 result대신 id element를 쓰면 성능이 향상된다고 한다.
fileName이 list타입이라 이것을 사용하게 된것이다.
collection이라는 element를 사용해야한다. 타입과 매핑될 컬럼을 지정해주면된다.
그리고 이 매핑정보를 사용하라고 id에 메소드 명을 맞추고 @ResultMap 어노테이션을 사용하면된다.
view에서는 board의 fileName의 list로 값을 꺼내면된다.
<resultMap type="com.example.demo.domain.Board" id="boardResultMap">
<id column="id" property="id" />
<result column="title" property="title" />
<result column="body" property="body" />
<result column="writer" property="writer" />
<result column="inserted" property="inserted" />
<collection property="fileName" ofType="string">
<result column="fileName" />
</collection>
</resultMap>@Select("SELECT b.id, b.title, b.body, b.writer, b.inserted, f.fileName FROM Board b "
+ "LEFT JOIN FileName f ON b.id = f.boardId "
+ "WHERE b.id = #{id}")
@ResultMap("boardResultMap")
Board selectById(Integer id);<!-- 그림 파일 출력 -->
<div class="mb-3">
<c:forEach items="${board.fileName }" var="fileName" >
<div>
<%-- http://localhost:8080/image/4122/slamdunk.jfif --%>
<%-- http://localhost:8080/image/게시물번호/fileName --%>
<img src="http://localhost:8080/image/${board.id }/${fileName}" alt="" />
</div>
</c:forEach>
</div>12.8.13 resultMap
resultMap매핑연습하기
SELECT c.CategoryID, c.CategoryName, c.Description, p.ProductName
FROM Categories c LEFT JOIN Products p
ON c.CategoryID = p.CategoryID;
의 내용을 가져온다고 생각해보자.
카테고리정보는 하나인데 여러번 반복해서 이름때문에 나타나게된다.
이만큼의 정보를 bean에 매핑하는 것 이것을 작성하는 방법이 xml이다.
resultMap element에 type은 매핑할 entity id는 resultMap의 이름
각 값을 넣는것은 result element list와 같은 컬렉션이라면 collection element
각 값의 column속성에 불러온 값의 이름 property에 entity에 매핑할 프로퍼티명
@Controller
@RequestMapping("sub31")
public class Controller31 {
@Autowired
private Mapper11 mapper;
@GetMapping("link1")
public void method1(int id) {
// 1번 카테고리 정보
Category category = mapper.sql1(id);
System.out.println(category);
category.getProducts().forEach(System.out::println);
}
}@Mapper
public interface Mapper11 {
@Select("""
SELECT c.CategoryId, c.CategoryName, c.Description, p.ProductName
FROM Categories c LEFT JOIN Products p
ON c.CategoryId = p.CategoryId
WHERE c.CategoryId = #{id}
""")
@ResultMap("categoryResultMap")
Category sql1(int id);
}<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.Mapper11">
<resultMap type="com.example.demo.domain.Category" id="categoryResultMap">
<id column="CategoryId" property="id"/>
<result column="CategoryName" property="name"/>
<result column="Description" property="description"/>
<collection property="products" ofType="String">
<result column="ProductName"/>
</collection>
</resultMap>
</mapper>12.8.14 resultMap 연습 - 2
복잡한것으로 연습해보자.
JOIN결과가 여러개로 나올 경우 javaBean에 매핑하는 방법
또 다른 필드를 추가한다고 햇을때를 보자.
SELECT c.CategoryID, c.CategoryName, c.Description, p.ProductName, p.Price
FROM Categories c LEFT JOIN Products p
ON c.CategoryID = p.CategoryID;두개의 필드가 상품정보라면 또다른 javaBean을 만들면된다.
객체가 담고있는 list정보를 카테고리가 가지게 되는 것이다.
@Data
public class Category {
private Integer id;
private String name;
private String description;
private List<Prodcut> products;
}@Data
public class Prodcut {
private String name;
private Double price;
}XML매핑만 어떻게할지 생각하면된다.
컬렉션에 매핑하는데 ofType에 LIST의 아이템 타입의 entity full name을 넣으면된다.
<resultMap type="com.example.demo.domain.Category" id="categoryResultMap">
<id column="categoryId" property="id"/>
<result column="categoryName" property="name"/>
<result column="description" property="description"/>
<collection property="products" ofType="com.example.demo.domain.Product">
<result column="productName" property="name"/>
<result column="price" property="price"/>
</collection>
</resultMap>javaBean에 javaBean을 담은 List를 넣는 것을 잘 생각해야한다.
12.8.15 resultMap 연습 - 3
Supplier 공급자가 공급하는 상품명을 알고싶다.
기존 DTO에 추가해도되지만 join을 위한 DTO를 새로 만들었다.
@Data
public class SupplerProduct {
private int supplierId;
private String supplierName;
private String contactName;
private String address;
private String city;
private String postalCode;
private String country;
private String phone;
private List<Product> products;
}@GetMapping("link3")
public void method3(int id) {
SupplerProduct supplier = mapper.sql3(id);
System.out.println(supplier);
supplier.getProducts().forEach(System.out::println);
}@Select("""
SELECT s.SupplierID, s.SupplierName, s.ContactName, s.Address, s.City, s.PostalCode, s.Country, s.Phone, p.ProductName, p.Price
FROM Suppliers s LEFT JOIN Products p
ON s.SupplierID = p.SupplierID
WHERE s.SupplierID = #{id}
""")
@ResultMap("supplierResult")
SupplerProduct sql3(int id);<resultMap type="com.example.demo.domain.SupplerProduct" id="supplierResult">
<id column="supplierId" property="supplierId"/>
<result column="supplierName" property="supplierName"/>
<result column="contactName" property="contactName"/>
<result column="address" property="address"/>
<result column="city" property="city"/>
<result column="postalCode" property="postalCode"/>
<result column="country" property="country"/>
<result column="phone" property="phone"/>
<collection property="products" ofType="com.example.demo.domain.Product">
<result column="ProductName" property="name"/>
<result column="price" property="price"/>
</collection>
</resultMap>테이블을 여러개 섞게 된다면 더 복잡해진다.
메뉴얼을 찾아서 해보거나 질문하기
1대 다 관계시 처리하는 법
12.8.16 파일
여러 게시물을 읽을때 어떤 글에 첨부파일이 있는지 없는지의 정보
몇개있는지
게시물별로 몇개있는지 를 아는 방법은 JOIN을 사용하면된다.
SELECT b.*, count(f.id) fileCount
FROM Board b LEFT JOIN FileName f ON b.id = f.boardId
GROUP BY b.id
ORDER BY b.id DESC
LIMIT 0, 10;BoardView DTO를 따로 만들어보았다.
SQL에서 BoardView로 View를 따로 만들어서 해도되지만
강의이기 때문에 일단 JOIN문을 전부 사용햇다.
진작에 인터페이스를 분리해놧기때문에 컨트롤러에서 오류가 나지 않는다.
서비스인터페이스는 그대로이기 때문에 서비스만 설정하면된다.
또한 sql만 변경하면된다.
view또한 변경할게 없다 따로 분리되어 있기 때문이다.
@Data
public class BoardView {
private Integer id;
private String title;
private String body;
private LocalDateTime inserted;
private String writer;
private List<String> fileName;
private Integer fileCount;
}@Select("""
<script>
<bind name="pattern" value="'%' + search + '%'"/>
SELECT b.*, count(f.id) fileCount
FROM Board b LEFT JOIN FileName f ON b.id = f.boardId
<where>
<if test="type eq 'all' or type eq 'title'">
title LIKE #{pattern}
</if>
<if test="type eq 'all' or type eq 'writer'">
OR writer LIKE #{pattern}
</if>
<if test="type eq 'all' or type eq 'body'">
OR body LIKE #{pattern}
</if>
</where>
GROUP BY b.id
ORDER BY b.id DESC
LIMIT #{startIndex}, #{rowPerPage}
</script>
""")
@ResultMap("boardViewMap")
List<BoardView> selectAllPaging(Integer startIndex, Integer rowPerPage, String search, String type);<resultMap type="com.example.demo.domain.BoardView" id="boardViewMap">
<id column="id" property="id" />
<result column="title" property="title" />
<result column="body" property="body" />
<result column="writer" property="writer" />
<result column="inserted" property="inserted" />
<result column="fileCount" property="fileCount"/>
</resultMap>12.9 view 수정
12.9.1 표시
https://getbootstrap.com/docs/5.3/components/badge/
파일 개수를 이쁘게 나타내 보자
배지에 이미지와 숫자를 넣어주자.
<c:if test="${board.fileCount > 0 }">
<span class="badge rounded-pill text-bg-info">
<i class="fa-sharp fa-solid fa-images"></i>
${board.fileCount}
</span>
</c:if>12.9.2 이미지
https://getbootstrap.com/docs/5.3/content/images/
그림사이에 테두리 넣기
이미지가 너무 크면 안넘어가게 역시나 부트 스트랩에서 가져오기
<!-- 그림 파일 출력 -->
<div class="mb-3">
<c:forEach items="${board.fileName }" var="fileName" >
<div>
<%-- http://localhost:8080/image/4122/slamdunk.jfif --%>
<%-- http://localhost:8080/image/게시물번호/fileName --%>
<img class="img-thumbnail img-fluid " src="http://localhost:8080/image/${board.id }/${fileName}" alt="" />
</div>
</c:forEach>
</div>12.9.3 파일선택
브라우저 기본스타일로 파일선택하는데 이것을 꾸며보기
<div class="mb-3">
<label for="formFile" class="form-label">첨부 파일</label>
<input class="form-control" name="files" type="file" id="formFile" accept="image/*" multiple>
</div>2023.05.02
gradle vs maven
파일을 저장해서 db에 이름으로 저장하는 방법을 배웠다.
일단은 서버에 저장하는 것이 아니라 내 하드에 저장하고 그것을 보여주는 것이다.
같은 이름의 파일이 잇을때 저장하는 방법에 대해서 고민햇엇는데
그냥 파일을 새로 만들어서 따로 관리한다는 점이 새로웠다.
View테이블을 따로 만들어서 각 값을 매핑하는 점 resultMap의 정확한 사용법을 알게 되었다.
resultMap을 왜 넣는지 정확히 이해가 안됫엇는데 직접 사용해보니 이해가 된다.
DTO안에 DTO를 담는 List를 사용하는 방법에 대해서 잘 알아보는 것이 중요해보인다.
'국비 > Project - 1 게시판' 카테고리의 다른 글
| 2023.05.04 70일차 Project (0) | 2023.05.04 |
|---|---|
| 2023.05.03 69일차 Project (0) | 2023.05.03 |
| 2023.05.01 67일차 Project (0) | 2023.05.01 |
| 2023.04.28 66일차 Project (0) | 2023.05.01 |
| 2023.04.27 65일차 Project (0) | 2023.04.27 |