[Spring Boot-JPA] csv 파일을 읽어 db에 저장하기 - opencsv

·2024년 3월 23일
1

안타깝게도 삽질이 되어버렸지만, 그래도.. 하긴 해봤으니까 기록이라도..

삽질 시작 계기

상점에 대한 데이터를 저장할 일이 생겼다.
상점에 대한 위치를 저장할 때, 주소에 대한 검증을 JPA에서 어떻게 해야할까?
'경기도' '서귀포시' 라고 들어오면 틀린 값인데, 이거 검증 어떻게 함?

미리 말하자면 오픈 api인 도로명주소를 사용하시면 됩니다

+) 악의적으로 api를 쏘면 어캄..어캄! : 이건 전 admin 계정에서만 접근 가능한 기능으로 구성함요

자 이걸 못떠올렸으니, 검증 두개재...~

  1. csv 파일로 도, 시를 작성(여기부터 걍 레전드 위키피디아에서 긁어왔음)
  2. csv 파일을 opencsv를 사용해 읽고, Province - City Entity 생성 후 저장하여 db 구축
  3. FK를 갖고 검증

csv는 찍먹 해보려고 데이터 긁어온 후 gpt햄한테 바꿔달라고 시킨거라
도 - 시까지만 뎁스를 잡음

ex)
도,시
경기도,수원시,용인시...
제주도,제주시,서귀포시

dependency에 opencsv 추가

    implementation 'com.opencsv:opencsv:5.5.2'

만약 ddl이 none일 경우를 대비해서 sql문 작성

// scheme.sql

CREATE TABLE IF NOT EXISTS Province
(
    korean_province_id BIGINT AUTO_INCREMENT PRIMARY KEY,
    name               VARCHAR(255) NOT NULL
);

CREATE TABLE IF NOT EXISTS City
(
    korean_city_id     BIGINT AUTO_INCREMENT PRIMARY KEY,
    name               VARCHAR(255) NOT NULL,
    korean_province_id BIGINT,
    FOREIGN KEY (korean_province_id) REFERENCES Province (korean_province_id)
);

Province - City 관계 설정

province는 하나, 속한 city는 여러개~
그럼 city가 FK로 province_id 갖고있게 만들고, 검증할 때 이거 쓰면 되겠다 ㅎㅎ 라는 행복회로

// Province.java
@Entity
@Getter
public class Province {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	@Column(name = "korean_province_id")
	private Long id;
	private String name;
}

// City.java
@Entity
@Getter
public class City {

	@Id
	@GeneratedValue(strategy = GenerationType.AUTO)
	@Column(name = "korean_city_id")
	private Long id;

	private String name;

	@ManyToOne(fetch = FetchType.LAZY)
	@JoinColumn(name = "korean_province_id")
	private Province province;
}

opencsv를 이용한 read-write Service


@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class CSVService {

	private final CityRepository cityRepository;
	private final ProvinceRepository provinceRepository;

	//application 실행 시 db 데이터 구축
	@PostConstruct
	public void AddressInit() {
		if (!dataAlreadyLoaded(provinceRepository)) {
			loadDataFromCSV("korean-address.csv");
		}
	}

	// 일단 임의로 작성하는 과정이라 데이터 개수를 셌지만, csv의 변경을 감지하는게 맞다 hash값?
	private boolean dataAlreadyLoaded(JpaRepository repository) {
		return repository.count() > 0;
	}

	@Transactional
	public void loadDataFromCSV(String fileName) {
		try {
        	// fileName 기준으로 파일을 가져온 후, FileReader 변환 -> 이걸 다시 CSVReader로 변환
			ClassLoader classLoader = getClass().getClassLoader();
			File file = new File(
				Objects.requireNonNull(classLoader.getResource(fileName)).getFile());
			FileReader reader = new FileReader(file);
			CSVReader csvReader = new CSVReader(reader);

			csvReader.readNext(); // 도, 시 한 줄 생략
			String[] nextRecord;
			while ((nextRecord = csvReader.readNext()) != null) {
            	// 모든 줄을 읽어서 컬럼 기준 첫번째는 '도'에 해당하는 이름
				String provinceName = nextRecord[0];
                // 없으면 만들고 저장
				Province province = provinceRepository.findByName(provinceName)
					.orElseGet(() -> {
						Province newProvince = Province.of(provinceName);
						return provinceRepository.save(newProvince);
					});
                    
                 List<City> result = new ArrayList<>();
                 // 컬럼 기준 쭉~ 뒤까지는 모두 '시'에 해당하는 이름들이니 province 객체와 함께 저장
				for (int i = 1; i < nextRecord.length; i++) {
					String cityName = nextRecord[i];
					City city = City.of(cityName, province);
					result.add(city);
				}
                // batch 사용 -> 쓰기 지연 후 일괄 저장
                cityRepository.saveAll(result);
			}
			csvReader.close();
		} catch (IOException | CsvValidationException e) {
			e.printStackTrace();
		}
	}

}

결과물

'도' 에 해당하는 province 테이블

'시'에 해당하는 city 테이블

여기까지 작성한 후, admin page 프론트 부분을 어떻게 구상해야하지? 떠올리던 중에서야 깨달음 ㅋㅋ

  1. 드롭다운 형식 -> 님 뭐 시,군,구 매번 눈알 빠지게 찾게 만들거임?
  2. String 입력받고 검증 -> 조금만 삐끗해도 잘못입력했다고 무한반려

아차차~ 대한민국에는 도로명주소 오픈 api가 있지~

삽질 그만하고 밥이나 먹으러 갔다

profile
어?머지?

0개의 댓글

관련 채용 정보

Powered by GraphCDN, the GraphQL CDN