JSON_TABLE 일괄 처리
게시글 배열을 행 집합으로 펼치고 항목별 오류를 보존한 뒤 검증이 끝난 배치만 posts에 적재합니다.
외부 편집 도구가 게시글 여러 건을 JSON 배열로 보내면 한 문서로 받는 것이 편합니다.
그렇다고 JSON 요청 본문을 그대로 영구 원본으로 삼으면 작성자 FK, 상태, 게시 시각, 멱등 요청 키 규칙을 쿼리마다 다시 구현해야 합니다.
반대로 JSON_TABLE 결과를 즉시 최종 테이블에 넣으면 어느 항목이 왜 오류했는지 남기기 어렵습니다.
안전한 가져오기는 원문 배치를 먼저 고정하고, 배열을 임시 적재 행으로 펼치며, 오류를 item_key별로 수집한 다음, 전체 검증이 끝난 경우에만 정규 스키마로 이동합니다.
같은 배치 UUID가 재전송되면 이미 만든 게시글 ID를 돌려줍니다.
요청 본문은 게시글 배열이며 각 항목은 itemKey, requestKey, title, content, status, publishedAt을 갖습니다.
JSON_TABLE은 명시한 타입의 열로 각 값을 투영하고 NULL ON ERROR를 사용해 문서 하나 때문에 파서가 즉시 중단되지 않게 합니다.
post_import_batch는 원본 JSON과 상태를, post_import_rejection은 항목 오류를, post_import_item은 외부 항목과 생성 게시글의 대응을 보존합니다.
완료된 배치의 원본 JSON은 수정하지 않습니다.
직접 정규화의 오류 추적 한계
JSON_TABLE의 타입 변환 오류가 구문을 중단하면 호출자는 잘못된 항목 위치를 추측해야 합니다.
변환을 느슨하게 하면 요청 키 누락, 허용하지 않은 상태, PUBLISHED인데 publishedAt이 없는 행이 최종 제약 조건에서 제각각 오류가 발생합니다.
SET @bad_payload=JSON_OBJECT('posts',JSON_ARRAY(
JSON_OBJECT('itemKey','a','requestKey','post-001','title','정상 게시글',
'content','본문','status','PUBLISHED','publishedAt','2026-07-17 10:00:00'),
JSON_OBJECT('itemKey','b','title','요청 키 없는 게시글',
'content','본문','status','PUBLISHED','publishedAt','not-a-date')
));
SELECT x.* FROM JSON_TABLE(@bad_payload,'$.posts[*]' COLUMNS(
item_key VARCHAR(80) PATH '$.itemKey',
request_key VARCHAR(80) PATH '$.requestKey',
title VARCHAR(200) PATH '$.title',
content TEXT PATH '$.content',
published_at DATETIME(6) PATH '$.publishedAt' ERROR ON ERROR
)) x;두 번째 항목의 날짜 변환에서 구문이 멈추고 다른 위반은 보고되지 않습니다.
오류 실행 결과parsed items before error=1
second item conversion=ERROR
missing request key diagnosis=not recorded
publish-time diagnosis=not recorded
retry identity=not recorded수신, 파싱, 업무 검증, 최종 쓰기를 한 구문에 겹치면 오류 경계가 하나뿐입니다. 임시 적재는 잘못된 값을 허용하기 위한 영구 쓰레기통이 아니라 오류를 구조화해 호출자에게 돌려주는 격리 구간입니다.
NULL ON ERROR를 선택하면 NULL이 원래 입력인지 변환 오류인지 원본 항목과 함께 판단해야 합니다.
필수 경로의 NULL, 원본 경로 존재, 타입 명시 결과를 조합해 오류 코드를 만듭니다.
배치 원자성과 부분 성공을 먼저 결정하기
이 예제는 한 항목이라도 잘못되면 최종 삽입을 0건으로 유지하는 전체 성공 또는 전체 취소 정책입니다. 부분 성공이 필요하면 항목 상태와 재제출 키를 별도로 설계하고 이미 성공한 항목을 중복 생성하지 않아야 합니다.
원본 문서와 정규 행
batch_id와 요청 본문 해시는 재전송 동일성을 판단합니다.
같은 UUID에 다른 요청 본문이 오면 재처리가 아니라 충돌입니다.
완료 배치의 항목 매핑은 클라이언트가 응답을 잃어도 게시글 ID를 다시 얻는 근거입니다.
JSON_TABLE은 문서를 관계형 행 집합으로 번역하는 경계입니다.
이후 작성자, 상태, 요청 키 검사는 평범한 SQL로 수행합니다.
posts에는 JSON 가져오기 세부를 복사하지 않고 배치/항목 매핑만 별도로 남깁니다.
오류 코드는 사용자 메시지와 분리하기
MISSING_REQUIRED, INVALID_STATUS, INVALID_PUBLISH_TIME 같은 안정된 코드를 저장하고 UI가 언어별 문구를 만듭니다.
원본 항목은 접근 권한과 보존 기간을 제한합니다.
MySQL 8.4 메모:
JSON_TABLE의COLUMNS에NULL ON ERROR를 명시해 엄격 변환 중단을 피합니다. 조용한 강제 변환을 성공으로 취급하지 않고 검증 쿼리가 NULL과 원문 경로를 검사합니다.
단계별 JSON 일괄 처리
프로시저는 임시 parsed_import를 만들고 필수값과 상태·게시 시각 조합을 검증합니다.
거절 사유가 있으면 배치 상태만 REJECTED로 커밋합니다.
오류가 없으면 커서가 각 게시글을 INSERT하고 LAST_INSERT_ID를 매핑에 기록합니다.
DROP PROCEDURE IF EXISTS process_post_import;
DROP TABLE IF EXISTS post_import_rejection;
DROP TABLE IF EXISTS post_import_item;
DROP TABLE IF EXISTS post_import_batch;
CREATE TABLE post_import_batch(
batch_id BINARY(16) PRIMARY KEY,author_id BIGINT UNSIGNED NOT NULL,
payload JSON NOT NULL,payload_sha256 BINARY(32) NOT NULL,
batch_status VARCHAR(16) NOT NULL DEFAULT 'STAGED',
created_at DATETIME(6) NOT NULL,processed_at DATETIME(6) NULL,
FOREIGN KEY(author_id) REFERENCES members(id) ON DELETE RESTRICT,
CHECK(batch_status IN('STAGED','REJECTED','COMPLETED'))
) ENGINE=InnoDB;
CREATE TABLE post_import_item(
batch_id BINARY(16) NOT NULL,item_key VARCHAR(80) NOT NULL,post_id BIGINT UNSIGNED NOT NULL,
PRIMARY KEY(batch_id,item_key),UNIQUE KEY uq_import_post(post_id),
FOREIGN KEY(batch_id) REFERENCES post_import_batch(batch_id) ON DELETE RESTRICT,
FOREIGN KEY(post_id) REFERENCES posts(id) ON DELETE RESTRICT
) ENGINE=InnoDB;
CREATE TABLE post_import_rejection(
batch_id BINARY(16) NOT NULL,item_key VARCHAR(80) NOT NULL,error_code VARCHAR(40) NOT NULL,
raw_item JSON NOT NULL,PRIMARY KEY(batch_id,item_key,error_code),
FOREIGN KEY(batch_id) REFERENCES post_import_batch(batch_id) ON DELETE CASCADE
) ENGINE=InnoDB;
DELIMITER //
CREATE PROCEDURE process_post_import(IN p_batch BINARY(16))
proc: BEGIN
DECLARE v_batch_status VARCHAR(16); DECLARE v_author BIGINT UNSIGNED; DECLARE v_payload JSON;
DECLARE v_done BOOLEAN DEFAULT FALSE; DECLARE v_batch_found BOOLEAN DEFAULT TRUE;
DECLARE v_key VARCHAR(80); DECLARE v_request_key VARCHAR(80);
DECLARE v_title VARCHAR(200); DECLARE v_content LONGTEXT; DECLARE v_post_status VARCHAR(16);
DECLARE v_published DATETIME(6); DECLARE v_post BIGINT UNSIGNED;
DECLARE cur CURSOR FOR
SELECT item_key,request_key,title,content,post_status,published_at
FROM parsed_import ORDER BY ordinal_no;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done=TRUE;
DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN ROLLBACK; RESIGNAL; END;
START TRANSACTION;
BEGIN
DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_batch_found=FALSE;
SELECT batch_status,author_id,payload INTO v_batch_status,v_author,v_payload
FROM post_import_batch WHERE batch_id=p_batch FOR UPDATE;
END;
IF v_batch_found=FALSE THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='import batch not found';
END IF;
IF v_batch_status='COMPLETED' THEN
COMMIT;
SELECT item_key,post_id FROM post_import_item WHERE batch_id=p_batch ORDER BY item_key;
LEAVE proc;
END IF;
DELETE FROM post_import_rejection WHERE batch_id=p_batch;
DROP TEMPORARY TABLE IF EXISTS parsed_import;
CREATE TEMPORARY TABLE parsed_import(
ordinal_no INT UNSIGNED,item_key VARCHAR(80),request_key VARCHAR(80),
title VARCHAR(200),content LONGTEXT,post_status VARCHAR(16),
published_at DATETIME(6),raw_item JSON,PRIMARY KEY(ordinal_no)
);
INSERT INTO parsed_import
SELECT j.ordinal_no,j.item_key,j.request_key,j.title,j.content,j.post_status,j.published_at,j.raw_item
FROM JSON_TABLE(v_payload,'$.posts[*]' COLUMNS(
ordinal_no FOR ORDINALITY,item_key VARCHAR(80) PATH '$.itemKey' NULL ON ERROR,
request_key VARCHAR(80) PATH '$.requestKey' NULL ON ERROR,
title VARCHAR(200) PATH '$.title' NULL ON ERROR,
content TEXT PATH '$.content' NULL ON ERROR,
post_status VARCHAR(16) PATH '$.status' NULL ON ERROR,
published_at DATETIME(6) PATH '$.publishedAt' NULL ON EMPTY NULL ON ERROR,
raw_item JSON PATH '#x27;
)) j;
INSERT INTO post_import_rejection
SELECT p_batch,COALESCE(item_key,CONCAT('#',ordinal_no)),'MISSING_REQUIRED',raw_item
FROM parsed_import
WHERE item_key IS NULL OR request_key IS NULL OR title IS NULL OR content IS NULL OR post_status IS NULL;
INSERT IGNORE INTO post_import_rejection
SELECT p_batch,COALESCE(item_key,CONCAT('#',ordinal_no)),'INVALID_STATUS',raw_item
FROM parsed_import
WHERE post_status NOT IN('DRAFT','PUBLISHED','HIDDEN');
INSERT IGNORE INTO post_import_rejection
SELECT p_batch,COALESCE(item_key,CONCAT('#',ordinal_no)),'INVALID_PUBLISH_TIME',raw_item
FROM parsed_import
WHERE (post_status='PUBLISHED')<>(published_at IS NOT NULL);
INSERT IGNORE INTO post_import_rejection
SELECT p_batch,COALESCE(p.item_key,CONCAT('#',p.ordinal_no)),'DUPLICATE_REQUEST_KEY',p.raw_item
FROM parsed_import p JOIN(
SELECT request_key FROM parsed_import WHERE request_key IS NOT NULL
GROUP BY request_key HAVING COUNT(*)>1
) d ON d.request_key=p.request_key;
INSERT IGNORE INTO post_import_rejection
SELECT p_batch,COALESCE(p.item_key,CONCAT('#',p.ordinal_no)),'ALREADY_EXISTS',p.raw_item
FROM parsed_import p JOIN posts x
ON x.author_id=v_author AND x.request_key=p.request_key;
IF EXISTS(SELECT 1 FROM post_import_rejection WHERE batch_id=p_batch) THEN
UPDATE post_import_batch SET batch_status='REJECTED',processed_at=CURRENT_TIMESTAMP(6)
WHERE batch_id=p_batch;
COMMIT; DROP TEMPORARY TABLE parsed_import;
SELECT item_key,error_code FROM post_import_rejection WHERE batch_id=p_batch ORDER BY item_key,error_code;
LEAVE proc;
END IF;
SET v_done=FALSE;
OPEN cur;
read_loop: LOOP
FETCH cur INTO v_key,v_request_key,v_title,v_content,v_post_status,v_published;
IF v_done THEN LEAVE read_loop; END IF;
INSERT INTO posts
(author_id,title,content,status,view_count,created_at,updated_at,published_at,version,request_key)
VALUES
(v_author,v_title,v_content,v_post_status,0,CURRENT_TIMESTAMP(6),CURRENT_TIMESTAMP(6),
v_published,0,v_request_key);
SET v_post=LAST_INSERT_ID();
INSERT INTO post_import_item VALUES(p_batch,v_key,v_post);
END LOOP;
CLOSE cur;
UPDATE post_import_batch SET batch_status='COMPLETED',processed_at=CURRENT_TIMESTAMP(6)
WHERE batch_id=p_batch;
COMMIT; DROP TEMPORARY TABLE parsed_import;
SELECT item_key,post_id FROM post_import_item WHERE batch_id=p_batch ORDER BY item_key;
END//
DELIMITER ;
INSERT INTO members(email,password_hash,name,status,created_at)
VALUES(
'ch18-import-author@example.com','not-a-real-hash',
'JSON 가져오기 예제 작성자','ACTIVE',CURRENT_TIMESTAMP(6)
)
ON DUPLICATE KEY UPDATE id=LAST_INSERT_ID(id);
SET @import_author_id=LAST_INSERT_ID();
DELETE FROM posts
WHERE author_id=@import_author_id
AND request_key LIKE 'ch18-2-%';
SET @completed_batch_id=UUID_TO_BIN('018f0000-0000-7000-8000-000000000201');
SET @completed_payload=JSON_OBJECT('posts',JSON_ARRAY(
JSON_OBJECT(
'itemKey','normal-1','requestKey','ch18-2-normal-1',
'title','게시판 사용 규칙','content','회원 게시판의 사용 규칙을 정리합니다.',
'status','PUBLISHED','publishedAt',
DATE_FORMAT(CURRENT_TIMESTAMP(6)+INTERVAL 1 DAY,'%Y-%m-%d %H:%i:%s.%f')
),
JSON_OBJECT(
'itemKey','normal-2','requestKey','ch18-2-normal-2',
'title','다음 글 초안','content','다음 게시글의 초안을 저장합니다.',
'status','DRAFT'
)
));
INSERT INTO post_import_batch
(batch_id,author_id,payload,payload_sha256,batch_status,created_at)
VALUES(
@completed_batch_id,@import_author_id,@completed_payload,
UNHEX(SHA2(CAST(@completed_payload AS CHAR),256)),
'STAGED',CURRENT_TIMESTAMP(6)
);
CALL process_post_import(@completed_batch_id);
SET @posts_before_replay=(
SELECT COUNT(*) FROM posts
WHERE author_id=@import_author_id
AND request_key IN('ch18-2-normal-1','ch18-2-normal-2')
);
CALL process_post_import(@completed_batch_id);
SET @posts_after_replay=(
SELECT COUNT(*) FROM posts
WHERE author_id=@import_author_id
AND request_key IN('ch18-2-normal-1','ch18-2-normal-2')
);
SET @rejected_batch_id=UUID_TO_BIN('018f0000-0000-7000-8000-000000000202');
SET @rejected_payload=JSON_OBJECT('posts',JSON_ARRAY(
JSON_OBJECT(
'itemKey','retry-1','requestKey','ch18-2-retry-1',
'title','회원 가입 안내','content','회원 가입 순서를 안내합니다.',
'status','PUBLISHED','publishedAt',
DATE_FORMAT(CURRENT_TIMESTAMP(6)+INTERVAL 1 DAY,'%Y-%m-%d %H:%i:%s.%f')
),
JSON_OBJECT(
'itemKey','retry-2',
'title','게시판 이용 안내','content','게시판 이용 순서를 안내합니다.',
'status','PUBLISHED'
)
));
INSERT INTO post_import_batch
(batch_id,author_id,payload,payload_sha256,batch_status,created_at)
VALUES(
@rejected_batch_id,@import_author_id,@rejected_payload,
UNHEX(SHA2(CAST(@rejected_payload AS CHAR),256)),
'STAGED',CURRENT_TIMESTAMP(6)
);
CALL process_post_import(@rejected_batch_id);
SET @retry_batch_id=UUID_TO_BIN('018f0000-0000-7000-8000-000000000203');
SET @retry_payload=JSON_SET(
@rejected_payload,
'$.posts[1].requestKey','ch18-2-retry-2',
'$.posts[1].publishedAt',
DATE_FORMAT(CURRENT_TIMESTAMP(6)+INTERVAL 1 DAY,'%Y-%m-%d %H:%i:%s.%f')
);
INSERT INTO post_import_batch
(batch_id,author_id,payload,payload_sha256,batch_status,created_at)
VALUES(
@retry_batch_id,@import_author_id,@retry_payload,
UNHEX(SHA2(CAST(@retry_payload AS CHAR),256)),
'STAGED',CURRENT_TIMESTAMP(6)
);
CALL process_post_import(@retry_batch_id);
SELECT fixture,batch_status,input_items,item_mappings,rejections
FROM(
SELECT
'normal' AS fixture,b.batch_status,
JSON_LENGTH(b.payload,'$.posts') AS input_items,
(SELECT COUNT(*) FROM post_import_item i WHERE i.batch_id=b.batch_id) AS item_mappings,
(SELECT COUNT(*) FROM post_import_rejection r WHERE r.batch_id=b.batch_id) AS rejections
FROM post_import_batch b WHERE b.batch_id=@completed_batch_id
UNION ALL
SELECT
'rejected',b.batch_status,JSON_LENGTH(b.payload,'$.posts'),
(SELECT COUNT(*) FROM post_import_item i WHERE i.batch_id=b.batch_id),
(SELECT COUNT(*) FROM post_import_rejection r WHERE r.batch_id=b.batch_id)
FROM post_import_batch b WHERE b.batch_id=@rejected_batch_id
UNION ALL
SELECT
'retry',b.batch_status,JSON_LENGTH(b.payload,'$.posts'),
(SELECT COUNT(*) FROM post_import_item i WHERE i.batch_id=b.batch_id),
(SELECT COUNT(*) FROM post_import_rejection r WHERE r.batch_id=b.batch_id)
FROM post_import_batch b WHERE b.batch_id=@retry_batch_id
) AS fixture_result
ORDER BY FIELD(fixture,'normal','rejected','retry');
SELECT @posts_after_replay-@posts_before_replay AS replay_created_posts;고정 fixture는 정상 배치, 게시 시각이 빠진 혼합 배치, 오류를 고쳐 새 UUID로 제출한 재시도 배치를 차례로 실행합니다.
정상 배치는 모든 항목과 매핑을 커밋하고 동일 UUID 재호출은 기존 ID만 반환합니다.
혼합 배치는 한 항목이 정상이어도 전체를 REJECTED로 남기며, 수정한 요청은 새 배치에서만 게시글을 만듭니다.
존재하지 않는 batch_id는 import batch not found 오류로 전체 트랜잭션을 롤백하며 COMPLETED 상태를 만들지 않습니다.
fixture batch_status input_items item_mappings rejections
normal COMPLETED 2 2 0
rejected REJECTED 2 0 2
retry COMPLETED 2 2 0
replay_created_posts=0커서는 대량 가져오기에서 느릴 수 있지만 항목과 자동 생성 ID 매핑을 명확히 보여 줍니다.
운영은 클라이언트가 제공한 안정적 requestKey를 (author_id, request_key) UNIQUE로 받아 집합 기반 INSERT 후 조인할 수 있습니다.
REJECTED 배치를 수정하지 않습니다.
클라이언트는 수정한 요청 본문에 새 batch_id를 사용합니다.
같은 ID의 원본 JSON을 덮으면 최초 오류 증거와 재시도 판단이 사라집니다.
요청 본문 해시의 충돌 검증
수신 엔드포인트는 INSERT 전에 정규화한 JSON 요청 본문의 SHA-256 해시를 계산합니다.
batch_id가 이미 있으면 저장된 해시와 비교하고 다르면 409 충돌을 반환합니다.
JSON 키 순서 정규화 규칙을 한 구현에서 고정합니다.
정상·혼합·재시도 검증
혼합 배치에는 요청 키 누락과 게시 시각 규칙 위반 항목을 넣습니다. 거절 두 코드가 저장되고 최종 게시글은 하나도 늘지 않아야 합니다. 완료 배치 재호출은 매핑 체크섬이 같습니다.
SELECT b.batch_id FROM post_import_batch b
WHERE b.batch_status='COMPLETED'
AND (SELECT COUNT(*) FROM post_import_item i WHERE i.batch_id=b.batch_id)
<> JSON_LENGTH(b.payload,'$.posts');
SELECT i.batch_id,i.item_key FROM post_import_item i
LEFT JOIN posts p ON p.id=i.post_id WHERE p.id IS NULL;
SELECT b.batch_id FROM post_import_batch b
WHERE b.batch_status='REJECTED'
AND EXISTS(SELECT 1 FROM post_import_item i WHERE i.batch_id=b.batch_id);
SELECT batch_id,item_key FROM post_import_item
GROUP BY batch_id,item_key HAVING COUNT(*)<>1;
SELECT p.author_id,p.request_key FROM posts p
GROUP BY p.author_id,p.request_key HAVING COUNT(*)>1;
SELECT
CASE batch_id
WHEN @completed_batch_id THEN 'normal'
WHEN @rejected_batch_id THEN 'rejected'
WHEN @retry_batch_id THEN 'retry'
END AS fixture,
batch_status,HEX(payload_sha256),processed_at
FROM post_import_batch
WHERE batch_id IN(@completed_batch_id,@rejected_batch_id,@retry_batch_id)
ORDER BY fixture;완료 항목 건수, 고아 매핑, REJECTED 배치의 부분 삽입, 중복 항목 쿼리는 모두 0행입니다.
세 배치 행에는 각 원본 JSON 해시와 처리 시각이 남습니다.
JSON_LENGTH가 NULL인 루트 구조는 수신 스키마에서 먼저 거절합니다.
이 프로시저에 루트 posts 배열 존재 검사를 추가하면 잘못된 배치도 REJECTED 상태와 사유를 가질 수 있습니다.
item_key 유일 범위
item_key는 배치 안에서만 유일합니다.
외부 앱이 전역 이벤트 ID를 제공한다면 UNIQUE(source_system, external_item_id)를 별도 원장에 두어 서로 다른 배치 중복도 막습니다.
대용량 문서 제한
한 요청 본문에 수십만 항목을 넣으면 행 잠금, 임시 테이블, 롤백 크기가 커집니다. API에서 항목 수와 바이트 크기를 제한하고 배치를 나눕니다. 전체 성공 또는 전체 취소 범위는 사용자가 수정 가능한 단위와 맞춰야 합니다.
원본 PII 보존
요청 본문에 자유 메모나 이메일이 들어올 수 있다면 거절된 raw_item도 개인정보입니다.
접근 권한, 암호화, 보존 기간과 파기 매핑을 설계합니다.
검증 편의를 이유로 무기한 보관하지 않습니다.
JSON 일괄 처리 범위
| 판단 축 | 확인할 질문 |
|---|---|
| 원자성 | 한 항목 오류 때 전체 배치를 취소해야 하는가? |
| 식별 | 클라이언트가 안정된 배치와 항목 키를 제공하는가? |
| 크기 | 임시 파싱과 롤백이 가능한 최대 항목 수를 정했는가? |
| 오류 | 호출자가 코드와 원본 위치로 수정할 수 있는가? |
| 정규화 | 성공 뒤 핵심 쿼리가 JSON이 아니라 FK 테이블을 사용하는가? |
일괄 업로드와 상세 오류가 필요하면 임시 적재 배치가 유용합니다.
실시간 단건 API라면 같은 구조를 더 작게 적용할 수 있습니다.
성공한 요청의 원문을 JSON에만 남기고 OLTP 쿼리가 매번 JSON_TABLE을 호출하는 방식은 피합니다.
연습 문제
동일 batch_id와 다른 요청 본문 해시가 들어오면 충돌을 기록하는 수신 프로시저를 작성하세요.
기존 원본 JSON과 상태는 절대 덮지 않아야 합니다.
해설과 예시 답안
INSERT를 시도하기 전에 배치 행을 잠그고 저장된 해시를 비교합니다. 같은 해시면 기존 상태를 반환하고 다른 해시면 SIGNAL을 발생시킵니다. 없는 ID만 STAGED로 생성합니다.
DROP PROCEDURE IF EXISTS stage_post_import;
DELIMITER //
CREATE PROCEDURE stage_post_import(
IN p_batch BINARY(16),IN p_author BIGINT UNSIGNED,
IN p_payload JSON,IN p_hash BINARY(32))
proc: BEGIN
DECLARE v_hash BINARY(32); DECLARE v_status VARCHAR(16);
START TRANSACTION;
SELECT payload_sha256,batch_status INTO v_hash,v_status
FROM post_import_batch WHERE batch_id=p_batch FOR UPDATE;
IF v_hash IS NOT NULL THEN
IF v_hash<>p_hash THEN ROLLBACK; SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='batch payload conflict'; END IF;
COMMIT; SELECT v_status AS batch_status; LEAVE proc;
END IF;
INSERT INTO post_import_batch
(batch_id,author_id,payload,payload_sha256,batch_status,created_at)
VALUES(p_batch,p_author,p_payload,p_hash,'STAGED',CURRENT_TIMESTAMP(6));
COMMIT; SELECT 'STAGED' AS batch_status;
END//
DELIMITER ;같은 해시 재호출은 행 수를 늘리지 않고 현재 상태를 반환합니다. 다른 해시는 배치 요청 본문의 충돌을 뜻하며 원래 JSON과 SHA-256 해시는 유지됩니다.
핵심 정리
JSON_TABLE은 문서 배열을 검증 가능한 관계형 행 집합으로 바꿉니다.- 원본 배치, 항목 거절, 최종 매핑은 서로 다른 수명과 책임을 가집니다.
- 오류 0건인 배치만 최종 게시글과 매핑을 한 트랜잭션에서 생성합니다.
- 배치 해시와 항목 키가 응답 손실·재전송에서 중복 생성을 막습니다.
다음 문서에서는 자주 검색하는 JSON 스칼라와 배열을 생성 열·다중 값 인덱스로 승격합니다.