[Gena Co.] Internship Project/GENA Labeling Tool

(25.03.01) ERD Update & 기능 개발(User, Sample, Template, Group CRUD)

Genie; 2025. 3. 5. 11:59

ERD 재정리

  • review_groups로 SQL 문법과 겹치지 않게 변경
  • review_groups 와 users 테이블이 N:M 관계를 형성을 해야했기 때문에 중간 테이블인 user_group테이블을 생성
  • 추후 라벨링 기능이 결정이 된다면 추가로 수정이 필요

User, Sample, Template, Group 테이블에 맞도록 각각의 CRUD 개발

  • domain/global 분리해서 개발

CSV File Upload 기능 

  • dataset 도메인에 CSV file upload 시 자동으로 sample 과 template에 해당하는  컬럼을 따로 분리해서 samples, review_groups 로 분리
 // Controller
 
 @PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<MessageResponseDto> uploadCsvFile(
            @Parameter(description = "CSV file to be uploaded") @RequestPart("file") MultipartFile file,
            @Parameter(description = "Metadata associated with the dataset") @RequestPart("metadata") DatasetMetadataDto metadata) throws Exception {
        datasetService.uploadCsvFile(file, metadata);
        return new ResponseEntity<>(new MessageResponseDto("Dataset uploaded successfully"), HttpStatus.CREATED);
    }

 

// Service
@Transactional
    public void uploadCsvFile(MultipartFile file, DatasetMetadataDto metadata) throws Exception {
        String datasetName = metadata.getDatasetName();
        String datasetDescription = metadata.getDatasetDescription();
		
        // Open CSV를 의존성 주입해서 CSV reader를 활용해 스트리밍으로 한 줄씩 읽어가면서(readNext() ) 진행 
        try (CSVReader csvReader = new CSVReader(new InputStreamReader(file.getInputStream()))) {
            String[] columns = csvReader.readNext();

            if (columns == null) throw new FileProcessingException("CSV file is empty");

            Map<String, Integer> columnIndexMap = new HashMap<>();

            for (int i = 0; i < columns.length; i++) {
                for (DatasetColumn column : DatasetColumn.values()) {
                    if (columns[i].equalsIgnoreCase(column.toString())) {
                        columnIndexMap.put(column.toString(), i);
                    }
                }
            }
			
            // 필요한 컬럼이 모두 있는지 검증이 필요
            if (columnIndexMap.size() != 4)
                throw new FileProcessingException("CSV file must contain 4 columns : sql_query, natural_question, no_template, sql_template");

            Integer noTemplateIndex = columnIndexMap.get(DatasetColumn.NO_SQL_TEMPLATE.toString());
            Integer sqlTemplateIndex = columnIndexMap.get(DatasetColumn.SQL_TEMPLATE.toString());

            String[] nextRecord;
            List<Sample> sampleList = new ArrayList<>();
            List<Template> templateList = new ArrayList<>();
            List<Long> templateNoList = new ArrayList<>();
			
            // 확인 된 내용을 Json 타입으로 바로 저장
            while ((nextRecord = csvReader.readNext()) != null) {
                JsonObject sampleData = new JsonObject();
                for (int i = 0; i < columns.length; i++) {
                    sampleData.addProperty(columns[i], nextRecord[i]);
                }

                Sample sample = Sample.builder()
                        .id(UUID.randomUUID().toString())
                        .datasetName(datasetName)
                        .datasetDescription(datasetDescription)
                        .versionId(1L)
                        .status(SampleStatus.CREATED)
                        .sampleData(sampleData.toString())
                        .build();

                sampleList.add(sample);
				
                // Template 테이블에도 해당 정보를 번호 기준 UNIQUE하게 저장
                Long templateNo = Long.parseLong(nextRecord[noTemplateIndex]);
                Template foundTemplate = templateRepository.findByTemplateNo(templateNo);

                if (foundTemplate == null && !templateNoList.contains(templateNo)) {
                    Template template = Template.builder()
                            .id(UUID.randomUUID().toString())
                            .templateNo(templateNo)
                            .content(nextRecord[sqlTemplateIndex])
                            .build();

                    templateList.add(template);
                    templateNoList.add(templateNo);
                }
            }

            sampleRepository.saveAll(sampleList);
            templateRepository.saveAll(templateList);

        } catch (Exception e) {
            throw new FileUploadException(e.getMessage(), e);
        }
    }

 

SQL_QUERY("sql_query"),
NATURAL_QUESTION("natural_question"),
NO_SQL_TEMPLATE("no_sql_template"),
SQL_TEMPLATE("sql_template");
  • CSV 파일에는 기본 ENUM으로 지정된 위의 이름의 컬럼이 있을 경우 template 테이블의 template_no 와 content에 저장할 수 있도록 했음
  • 위의 SQL QUERY 와 NATURAL QUESTION 이 Reviewer 가 확인하고, 수정, 또는 라벨링 할 수 있는 항목이기 때문에 반드시 존재해야 하는 컬럼
    • 따로 테이블에 저장하는 것이 아니라 있는지 검증 후, JSON 형태로 sample_data컬럼에 저장할 수 있도록 했음

Repository에서의 Native SQL 활용

public interface SampleRepository extends JpaRepository<Sample, String> {

    @Query(value = "SELECT s.* FROM samples s " +
            "WHERE s.status IN ('UPDATED','DELETED','CREATED') " +
            "AND s.version_id = (SELECT MAX(s2.version_id) FROM samples s2 WHERE JSON_UNQUOTE(JSON_EXTRACT(s2.sample_data, '$.id')) = JSON_UNQUOTE(JSON_EXTRACT(s.sample_data, '$.id'))) " +
            "ORDER BY JSON_UNQUOTE(JSON_EXTRACT(s.sample_data, '$.id')) ASC", nativeQuery = true)
    List<Sample> findLatestUpdatedSample();

    @Query(value = "SELECT s.* FROM samples s " +
            "WHERE JSON_UNQUOTE(JSON_EXTRACT(s.sample_data, '$.id')) = ?1 " +
            "AND s.status IN ('UPDATED', 'DELETED', 'CREATED') " +
            "ORDER BY s.version_id DESC " +
            "LIMIT 1", nativeQuery = true)
    Optional<Sample> findLatestBySampleId(String sampleId);

    @Query(value = "SELECT s.* FROM samples s " +
            "WHERE s.status LIKE 'REQUESTED_%' " +
            "ORDER BY JSON_UNQUOTE(JSON_EXTRACT(s.sample_data, '$.id')) ASC, s.version_id ASC", nativeQuery = true)
    List<Sample> findRequestedSample();

    @Query(value = "SELECT s.* FROM samples s " +
            "WHERE JSON_UNQUOTE(JSON_EXTRACT(s.sample_data, '$.id')) = ?1 " +
            "AND s.status LIKE 'REQUESTED_%' " +
            "And s.version_id = ?2", nativeQuery = true)
    List<Sample> findRequestedBySampleIdAndVersionId(String sampleId, Long versionId);

    @Query(value = "SELECT u.* " +
            "FROM users u " +
            "JOIN user_group ug ON u.id = ug.user_id " +
            "JOIN review_groups g ON ug.group_id = g.id " +
            "WHERE g.id = (SELECT s.group_id FROM samples s WHERE s.id = ?1 LIMIT 1)",
            nativeQuery = true)
    List<User> findUsersAssignedToSample(String sampleId);

    @Query(value = "SELECT * FROM samples " +
            "WHERE version_id = ?1 " +
            "AND id != ?2 " +
            "AND status != 'CREATED' " +
            "ORDER BY updated_at",
            nativeQuery = true)
    List<Sample> getOtherSamplesOfSameVersion(Long versionId, String sampleId);

    @Query(value = """
    SELECT sub.*
    FROM (
        SELECT s.*, 
               ROW_NUMBER() OVER (
                   PARTITION BY JSON_UNQUOTE(JSON_EXTRACT(s.sample_data, '$.id')) 
                   ORDER BY s.updated_at DESC, s.version_id DESC
               ) AS rn
        FROM samples s
        JOIN user_group ug ON ug.group_id = s.group_id
        WHERE ug.user_id = ?1
          AND (
              s.updated_by = ?1
              OR (COALESCE(s.updated_by, '') != ?1 AND s.status = 'CREATED')
          )
    ) sub
    WHERE sub.rn = 1
    ORDER BY JSON_UNQUOTE(JSON_EXTRACT(sub.sample_data, '$.id')) ASC
    """, nativeQuery = true)
    List<Sample> findAllByUserId(String userId);
}
  • Sample을 필터링하거나 특정 조건으로 조회하기 위해서는 sample_data값에서의 특정 필드값을 확인하는 작업이 필요하기 때문에, 서비스단의 비지니스로직에서 활용하는 것보다 MySQL DB에서 해당 Native Query를 Spring Data JPA의 JPQL 보다는 좀더 정교하게 native query를 활용해서 ROW_NUMBER() 등 윈도우 함수를 활용하고자 했음
    • text2sql 앱이기 때문에 협업 개발자의 이해를 좀더 쉽게 돕기 위해 SQL언어를 활용했음