트랜잭션
만약 회원과 카드, 취미 테이블을 정보를 저장하는 작업을 할 시 하나라도 실패하면 저장을 모두 하지 말아야한다
회원 > 취미 > 카드 작업 중에 카드 저장이 실패된 경우
카드는 저장되지 않았지만 회원과 취미는 저장된 상태가 된다.
하나라도 실패할 시 한번에 관리해야되므로
이러한 현상을 관리해주기 위해서 트랜잭션을 관리해야하며
애노테이션
@Transactional
를 통해 이루어진다
초기 설정
root-context.xml
bean에 추가
<!-- 추가 -->
xmlns:tx="http://www.springframework.org/schema/tx"
<!-- 경로 변경 -->
xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.1.xsd"
<!-- 트랜잭션 관리자의 빈을 정의 -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 애너테이션 기반의 트랜잭션 제어를 활성화함 -->
<tx:annotation-driven/>
더보기
더보기
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- Root Context: defines shared resources visible to all other web components -->
<!--
root-context.xml : 스프링 설정 파일
서블릿과 필터가 공유할 수 있는 루트 스프링 컨테이너 설정으로,
공통 빈(Service, Repository(DAO), DB, Log 등)을 설정함.
공통빈을 설정하는 곳으로 주로 View 지원을 제외한 bean을 설정함
스프링 설정?
view와 관련되지 않은 객체를 정의
Service(기능), DAO(Repository : 저장소), DB등 비즈니스 로직과 관련된 설정
BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName() = "oracle.jdbc.driver.OracleDriver";
-->
<!-- dataSource : 데이터베이스와 관련된 정보를 설정 -->
<!--
db : database(개념. 공유/저장/통합/운영). RDB(Relational DB.관계형DB)
dbms : database management system(DB관리시스템.오라클)
localhost=127.0.0.1=내ip주소
xe : express(OracleXE11g.r2) => SID(sequence ID)
-->
<bean id="uploadFolder" class="java.lang.String">
<constructor-arg value="C:\\Users\\PC-13\\git\\repository4\\springProj\\src\\main\\webapp\\resources\\upload"></constructor-arg>
</bean>
<bean id="uploadFolderDirect" class="java.lang.String">
<constructor-arg value="C:\\eGovFrameDev-3.10.0-64bit\\workspace\\.metadata\\.plugins\\org.eclipse.wst.server.core\\tmp0\\wtpwebapps\\springProj\\resources\\upload"></constructor-arg>
</bean>
<!--
BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName("oracle.jdbc.driver.OracleDriver");
dataSource.setUrl("jdbc:oracle:thin:@localhost:1521:xe");
-->
<bean id="dataSource"
class="org.apache.commons.dbcp2.BasicDataSource"
destroy-method="close">
<property name="driverClassName"
value="oracle.jdbc.driver.OracleDriver" />
<property name="url"
value="jdbc:oracle:thin:@localhost:1521:xe" />
<property name="username" value="jsptest" />
<property name="password" value="java" />
</bean>
<!-- 데이터베이스와 연결을 맺고 끊어질 때까지의
라이프 사이클을 관리해주는 sqlSession 객체를 생성
1) dataSource
2) 매퍼 xml의 위치 지정. / : src/main/resources/
SqlSessionFactoryBean sqlSessionFactory = new SqlSessionFactoryBean();
sqlSessionFactory.setDataSource(dataSource);
-->
<bean id="sqlSessionFactory"
class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"></property>
<property name="mapperLocations"
value="classpath:/sqlmap/**/*_SQL.xml" />
<property name="configLocation"
value="/WEB-INF/mybatisAlias/mybatisAlias.xml" />
</bean>
<!-- 데이터베이스에 개별적으로 쿼리를 실행시키는 객체.
이 객체를 통해 query를 실행함
-->
<bean id="sqlSessionTemplate"
class="org.mybatis.spring.SqlSessionTemplate">
<constructor-arg index="0" ref="sqlSessionFactory" />
</bean>
<!-- 파일업로드 설정
CommonsMultipartResolver multipartResolver = new multipartResolver();
multipartResolver.setMaxUploadSize(10485760);
multipartResolver.setDefaultEncoding("UTF-8");
-->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<!-- 파일업로드 용량 (10MB)-->
<property name="maxUploadSize" value="10485760"/>
<property name="defaultEncoding" value="UTF-8" />
</bean>
<!-- 파일업로드 디렉토리 설정 -->
<bean id="uploadPath" class="java.lang.String">
<constructor-arg value="c:\\upload"/>
</bean>
<!-- Mapper 인터페이스 설정
개발자가 직접 DAO를 설정하지 않아도
자동으로 Mapper 인터페이스를 활용하는 객체를 생성하게 됨
-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="kr.or.ddit.**.mapper" />
</bean>
<!-- 트랜잭션 관리자의 빈을 정의 -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 애너테이션 기반의 트랜잭션 제어를 활성화함 -->
<tx:annotation-driven/>
</beans>
예시
ProdServiceImpl.java
package kr.or.ddit.service.impl;
import java.io.File;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import kr.or.ddit.dao.ProdDao;
import kr.or.ddit.mapper.FileGroupMapper;
import kr.or.ddit.mapper.ProdMapper;
import kr.or.ddit.service.ProdService;
import kr.or.ddit.util.UploadController;
import kr.or.ddit.vo.BuyerVO;
import kr.or.ddit.vo.CardVO;
import kr.or.ddit.vo.CarsVO;
import kr.or.ddit.vo.FileDetailVO;
import kr.or.ddit.vo.FileGroupVO;
import kr.or.ddit.vo.LprodVO;
import kr.or.ddit.vo.MemberVO;
import kr.or.ddit.vo.ProdVO;
import kr.or.ddit.vo.TblHobbyVO;
import kr.or.ddit.vo.TblUserVO;
@Service
public class ProdServiceImpl implements ProdService{
@Autowired
ProdMapper mapper;
@Autowired
String uploadPath;
@Autowired
FileGroupMapper fileMapper;
@Autowired
UploadController upload;
// 회원 정보를 저장하다가 실패하거나 카드 등록 실패하면 모두 취소
// 모두 저장에 성공해야 성공하도록 처리
@Transactional
@Override
public int cardFormPost(TblUserVO vo) {
// 1) tbl_user 테이블 insert
int result = this.mapper.insertTblUser(vo);
// 2) card 테이블에 insert
List<CardVO> cardvo = vo.getCardVoList();
String userId = vo.getUserId();
for(CardVO v : cardvo) {
v.setUserId(userId);
result+= this.mapper.insertCard(v);
}
// 3) hobby에 insert
List<TblHobbyVO> hobbyvo = new ArrayList<TblHobbyVO>();
String[] hobby = vo.getHobby(); // hobby 배열을 사용
// String[] hobby = vo.getHobbyStr().split(","); // hobbyStr을 활용
if(hobby!=null) {
for(String h: hobby) {
TblHobbyVO hvo = new TblHobbyVO();
hvo.setUserId(userId);
hvo.setHobby(h);
hobbyvo.add(hvo);
result+= this.mapper.insertHobby(hvo);
}
}
// 4) cars에 insert
List<CarsVO> carsVoList = new ArrayList<CarsVO>();
String[] cars = vo.getCars();
for(String car: cars) {
CarsVO cvo = new CarsVO();
cvo.setUserId(userId);
cvo.setCar(car);
carsVoList.add(cvo);
}
result+=this.mapper.insertCars(carsVoList);
return result;
}
}
UploadController.java
package kr.or.ddit.util;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import kr.or.ddit.mapper.FileGroupMapper;
import kr.or.ddit.vo.FileDetailVO;
import kr.or.ddit.vo.FileGroupVO;
import lombok.extern.slf4j.Slf4j;
//자바빈 객체로 등록
@Slf4j
@Controller
public class UploadController {
//DI, IoC
//root-context.xml에 <bean.. 으로 존재함
@Autowired
String uploadPath;
@Autowired
FileGroupMapper fileGroupMapper;
/**다중 이미지 업로드
* return : 20240808001(FILE_GROUP.FILE_GROUP_NO)
*/
@Transactional
public long multiImageUpload(MultipartFile[] multipartFiles) {
long fileGroupNo = 0L;
int result = 0;
int counter = 1;//FILE_SN 컬럼을 위함(20240808001의1)(20240808001의2)
//연월일 폴더 설정
// C:\\upload
// + "\\" +
// 2024\\08\\08
File uploadPath = new File(this.uploadPath, this.getFolder());
if(uploadPath.exists()==false) {
uploadPath.mkdirs();
}
//원본 파일명
String fileName = "";
//MIME(Multipurpost Internet Mail Extension) 타입
String contentType = "";
//파일 크기
long fileSize = 0L;
//1) FILE_GROUP 테이블에 insert
FileGroupVO fileGroupVO = new FileGroupVO();
result += this.fileGroupMapper.insertFileGroup(fileGroupVO);
//향상된 for문
for(MultipartFile multipartFile : multipartFiles) {
fileName = multipartFile.getOriginalFilename();
contentType = multipartFile.getContentType();
fileSize = multipartFile.getSize();
UUID uuid = UUID.randomUUID();
fileName = uuid.toString() + "_" + fileName;
//File객체 설계(복사할 대상 경로, 파일명)
// C:\\upload\\2024\\08\\08 + "\\" + asdfasdf_개똥이.jpg
File saveFile = new File(uploadPath,fileName);
try {
//파일 복사 실행
//파일객체.transferTo(설계)
multipartFile.transferTo(saveFile);
//실행전 {fileGroupNo=0,fileRegdate=null}
//실행후 {fileGroupNo=20240808001,fileRegdate=null}
//2) FILE_DETAIL 테이블에 insert
FileDetailVO fileDetailVO = new FileDetailVO();
fileDetailVO.setFileSn(counter++);
fileDetailVO.setFileGroupNo(fileGroupVO.getFileGroupNo());
fileDetailVO.setFileOriginalName(multipartFile.getOriginalFilename());
fileDetailVO.setFileSaveName(fileName);//uuid + 파일명
// /upload/ == C:\\upload\\ + 2024\\08\\08 + "\\" + asdfasdf_파일명.jpg
fileDetailVO.setFileSaveLocate("/upload/" +
this.getFolder().replace("\\", "/") +
"/" + fileName
);
fileDetailVO.setFileSize(fileSize);
fileDetailVO.setFileExt(
fileName.substring(fileName.lastIndexOf(".")+1));//asdfasdf_파일명.jpg
fileDetailVO.setFileMime(contentType);
fileDetailVO.setFileFancysize(
makeFancySize(String.valueOf(fileSize))
);//bytes -> MB
fileDetailVO.setFileSaveDate(null);
fileDetailVO.setFileDowncount(0);
result += this.fileGroupMapper.insertFileDetail(fileDetailVO);
fileGroupNo = fileGroupVO.getFileGroupNo();
} catch (IllegalStateException | IOException e) {
log.error(e.getMessage());
}
}
log.info("result : " + result);
return fileGroupNo;
}
//연/월/일 폴더 생성
public String getFolder() {
//2022-11-16 형식(format) 지정
//간단한 날짜 형식
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
//날짜 객체 생성(java.util 패키지)
Date date = new Date();
//2022-11-16
String str = sdf.format(date);
//2024-01-30 -> 2024\\01\\30
return str.replace("-", File.separator);
}
//이미지인지 판단. 썸네일은 이미지만 가능하므로..
public boolean checkImageType(File file) {
//MIME(Multipurpose Internet Mail Extensions) : 문서, 파일 또는 바이트 집합의 성격과 형식. 표준화
//MIME 타입 알아냄. .jpeg / .jpg의 MIME타입 : image/jpeg
String contentType;
try {
contentType = Files.probeContentType(file.toPath());
log.info("contentType : " + contentType);
//image/jpeg는 image로 시작함->true
return contentType.startsWith("image");
} catch (IOException e) {
e.printStackTrace();
}
//이 파일이 이미지가 아닐 경우
return false;
}
//fancySize 리턴("1059000")
public String makeFancySize(String bytes) {
log.info("bytes : " + bytes);
String retFormat = "0";
//숫자형문자->실수형으로 형변환(1059000)
Double size = Double.parseDouble(bytes);
String[] s = { "bytes", "KB", "MB", "GB", "TB", "PB" };
if (bytes != "0") {
//bytes->KB
int idx = (int) Math.floor(Math.log(size) / Math.log(1024));
DecimalFormat df = new DecimalFormat("#,###.##");
double ret = ((size / Math.pow(1024, Math.floor(idx))));
retFormat = df.format(ret) + " " + s[idx];
} else {
retFormat += " " + s[0];
}
return retFormat;
}
@Transactional
public int deleteFile(FileGroupVO fgvo) {
int result = 0;
// 파일 삭제
List<FileDetailVO> list = fgvo.getFileDetailVoList();
for(FileDetailVO fd : list) {
String path = "C:"+fd.getFileSaveLocate().replace("/", "\\");
File file = new File(path);
file.delete();
}
// 파일 detail 삭제
result=this.fileGroupMapper.fileDetailDelete(fgvo);
// 파일 group 삭제
result=this.fileGroupMapper.fileGroupDelete(fgvo);
return result;
};
}
'Spring' 카테고리의 다른 글
[Spring] 시큐리티 (0) | 2024.08.09 |
---|---|
[Spring] 입력값 검증 (Validator) (0) | 2024.08.09 |
[Spring] 상품 상세 - 수정,삭제 (0) | 2024.08.09 |
[Spring] 상품 상세 정보 (Detail) (0) | 2024.08.08 |
[Spring] 상품 추가에 상품 이미지 미리보기 (0) | 2024.08.08 |