비정형 데이터를 다루는 데이터베이스 - 수정

Pt J·2026년 4월 22일
post-thumbnail

비정형 데이터를 다루는 데이터베이스 - 수정

비정형 데이터 JSON의 내용을 수정하는 방법을 알아보자.

에셋의 모든 정보를 바꾸기보다,
특정 메타데이터만 추가하거나 수정하는 일이 훨씬 많다.
PostgreSQL의 || (병합) 연산자를 사용하면
기존 JSON 데이터를 유지하면서 특정 키만 덮어쓰거나 추가할 수 있다.

최상위 키만 비교하기 때문에 중첩된 데이터가 있을 경우
일부 데이터가 누락될 수 있는데 이를 방지하려면
jsonb_set 함수를 중첩으로 사용해야 하지만
일반적인 태그 추가나 상태 변경은 || 로도 충분하다.

주의!
기존 메타데이터가 {"info": {"color": "red"}} 인데
{"info": {"size": 10}} 을 병합하면
info 내부가 합쳐지는 게 아니라 info 전체가 새 객체로 덮어씌워짐.

코드 작성

Rust 코드

sqlx::query_as! 를 사용하면
두 번째 인자로 전달된 쿼리의 결과를
첫 번째 인자로 전달된 변수에 바로 대입할 수 있다.
세 번째 인자부터는 sqlx::query! 와 마찬가지로
쿼리에 대입되는 값이다.
UPDATE 연산은 RETURNING 을 통해 결과를 반환한다.

src/image.rs

use pyo3::prelude::*;
use pyo3::exceptions::PyRuntimeError;
use sqlx::{Pool, Postgres};
use uuid::Uuid;
use serde_json::Value as JsonValue;
use tracing::{info, instrument, error};

#[pyclass(dict)]
#[derive(Clone, serde::Serialize, serde::Deserialize, sqlx::FromRow)]
pub struct ImageAsset {
    pub id: Uuid,
    #[pyo3(get)]
    pub file_name: String,
    #[pyo3(get)]
    pub file_path: String,
    #[pyo3(get)]
    pub width: i32,
    #[pyo3(get)]
    pub height: i32,
    pub metadata: JsonValue,
}

#[pymethods]
impl ImageAsset {
    #[getter]
    fn id(&self) -> String {
        self.id.to_string()
    }

    #[getter]
    fn metadata<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
        pythonize::pythonize(py, &self.metadata)
            .map_err(|e| PyRuntimeError::new_err(e.to_string()))
    }

    fn to_dict<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
        pythonize::pythonize(py, self)
            .map_err(|e| PyRuntimeError::new_err(e.to_string()))
    }
}

// 기존 함수 생략

#[instrument(skip(pool))]
pub async fn update_metadata(
    pool: &Pool<Postgres>,
    id: uuid::Uuid,
    new_meta: serde_json::Value,
) -> Result<ImageAsset, sqlx::Error> {
    let updated = sqlx::query_as!(
        ImageAsset,
        r#"
        UPDATE image_assets
        SET metadata = metadata || $1
        WHERE id = $2
        RETURNING id, file_name, file_path, width, height, metadata
        "#,
        new_meta,
        id
    )
    .fetch_one(pool)
    .await?;

    Ok(updated)
}

src/lib.rs

mod db;
mod logger;
mod image;

use pyo3::prelude::*;
use tokio::runtime::Runtime;
use dotenvy::dotenv;
use std::env;
use std::sync::OnceLock;
use tracing::{debug, error, info, instrument};
use tracing_appender::non_blocking::WorkerGuard;

static LOG_GUARD: OnceLock<WorkerGuard> = OnceLock::new();
static TOKIO_RUNTIME: OnceLock<Runtime> = OnceLock::new();

fn get_runtime() -> &'static Runtime {
    TOKIO_RUNTIME.get_or_init(|| {
        Runtime::new().expect("Tokio 런타임 생성 실패")
    })
}

// 기존 함수 생략

#[pyfunction]
fn patch_asset_metadata(
    id_str: String,
    metadata_json: String
) -> PyResult<image::ImageAsset> {
    let rt = get_runtime();
    let pool = db::DB_POOL.get()
        .ok_or(pyo3::exceptions::PyRuntimeError::new_err("DB 연결 풀이 없습니다."))?;

    let id = uuid::Uuid::parse_str(&id_str)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;

    let metadata: serde_json::Value = serde_json::from_str(&metadata_json)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;

    let asset = rt.block_on(async {
        image::update_metadata(pool, id, metadata).await
    }).map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;

    Ok(asset)
}

#[pymodule]
fn rust_engine(m: &Bound<'_, PyModule>) -> PyResult<()> {
    m.add_function(wrap_pyfunction!(init_engine, m)?)?;
    m.add_function(wrap_pyfunction!(shutdown_engine, m)?)?;
    m.add_function(wrap_pyfunction!(check_connection, m)?)?;
    m.add_function(wrap_pyfunction!(add_image_asset, m)?)?;
    m.add_function(wrap_pyfunction!(search_assets, m)?)?;
    m.add_function(wrap_pyfunction!(patch_asset_metadata, m)?)?;

    Ok(())
}

Python 코드

app/main.py

from fastapi import FastAPI, HTTPException
from fastapi.responses import ORJSONResponse
from contextlib import asynccontextmanager
import rust_engine
import logging
import json
from pydantic import BaseModel

# 기존 함수 생략

@app.patch("/assets/{asset_id}/metadata")
async def update_asset(asset_id: str, payload: dict):
    updated = rust_engine.patch_asset_metadata(asset_id, json.dumps(payload))

    return {
        "status": "success",
        "data": updated.to_dict()
    }

빌드 및 실행

Maturin 라이브러리를 통해 Rust 코드를 Python에서 호출 가능한 형태로 컴파일한다.
pyproject.toml 파일에 build-backend = "maturin" 이 명시되어 있으니
uv를 사용하는 방식으로 컴파일하겠다.

uvicorn 라이브러리를 통해 FastAPI를 실행한다.

~/workspace/image-assets-management$ uv pip install -e .
~/workspace/image-assets-management$ uv run uvicorn app.main:app --reload

프론트엔드 단에서 /assets/search 로 조회하여
원하는 이미지 에셋의 id 를 파악하였고
그것에 대한 수정을 하는 것으로 가정하여
id 는 호출하는 쪽에서 알고 있는 상황을 상정한다.

실행 예시에서는 데이터 변화를 확인하기 위해 조회도 수행하겠다.

~$ curl -i "http://127.0.0.1:8000/assets/search?name_query=hero_char"
HTTP/1.1 200 OK
date: Wed, 22 Apr 2026 01:39:55 GMT
server: uvicorn
content-length: 252
content-type: application/json; charset=utf-8

{"total_count":1,"items":[{"id":"019d8eae-5943-76c4-9a85-195ce2c4f81a","file_name":"hero_char_diffuse.exr","file_path":"/assets/textures/hero/","width":4096,"height":4096,"metadata":{"avg_luminance":0.45,"format":"EXR","layers":32}}],"page":1,"size":5}%         ``` 
~$ curl -iX PATCH "http://127.0.0.1:8000/assets/019d8eae-5943-76c4-9a85-195ce2c4f81a/metadata" \
     -H "Content-Type: application/json" \
     -d '{"author": "peter", "is_verified": true}'
HTTP/1.1 200 OK
date: Wed, 22 Apr 2026 01:40:13 GMT
server: uvicorn
content-length: 270
content-type: application/json; charset=utf-8

{"status":"success","data":{"id":"019d8eae-5943-76c4-9a85-195ce2c4f81a","file_name":"hero_char_diffuse.exr","file_path":"/assets/textures/hero/","width":4096,"height":4096,"metadata":{"author":"peter","avg_luminance":0.45,"format":"EXR","is_verified":true,"layers":32}}}%
~$ curl -i "http://127.0.0.1:8000/assets/search?name_query=hero_char"                           
HTTP/1.1 200 OK
date: Wed, 22 Apr 2026 01:40:20 GMT
server: uvicorn
content-length: 288
content-type: application/json; charset=utf-8

{"total_count":1,"items":[{"id":"019d8eae-5943-76c4-9a85-195ce2c4f81a","file_name":"hero_char_diffuse.exr","file_path":"/assets/textures/hero/","width":4096,"height":4096,"metadata":{"author":"peter","avg_luminance":0.45,"format":"EXR","is_verified":true,"layers":32}}],"page":1,"size":5}% 

메타데이터가 수정된 것을 확인할 수 있다.

profile
Peter J Online Space - since July 2020 | 아무데서나 채용해줬으면 좋겠다

0개의 댓글