네이티브 SQL을 사용하여 Firebase Data Connect 작업 구현

Firebase Data Connect는 Cloud SQL 데이터베이스와 상호작용하는 여러 방법을 제공합니다.

  • 네이티브 GraphQL: schema.gql에서 유형을 정의하면 GraphQL 작업이 SQL로 변환됩니다.Data Connect 이는 강력한 유형 지정과 스키마 적용 구조를 제공하는 표준 접근 방식입니다. 이 페이지를 제외한 대부분의 Data Connect 문서에서는 이 옵션을 설명합니다. 가능한 경우 이 방법을 사용하여 완전한 유형 안전성과 도구 지원을 활용해야 합니다.
  • @view 지시어: 맞춤 SELECT SQL 문으로 지원되는 schema.gql의 GraphQL 유형을 정의합니다. 이는 복잡한 SQL 논리를 기반으로 강력한 유형의 읽기 전용 뷰를 만드는 데 유용합니다. 이러한 유형은 일반 유형처럼 쿼리할 수 있습니다. @view을 참고하세요.
  • 네이티브 SQL: .에서 명명된 작업에 SQL 문을 직접 삽입합니다.특수 루트 필드를 사용하는 gql 파일 이를 통해 특히 표준 GraphQL로 쉽게 표현되지 않는 작업, 데이터베이스별 기능 활용, PostgreSQL 확장 프로그램 활용 시 최대한의 유연성과 직접 제어가 가능합니다.

이 가이드에서는 네이티브 SQL 옵션을 중점적으로 다룹니다.

네이티브 SQL의 일반적인 사용 사례

네이티브 GraphQL은 완전한 유형 안전성을 제공하고 @view 지시어는 읽기 전용 SQL 보고서에 강력한 유형의 결과를 제공하지만 네이티브 SQL은 다음을 위해 필요한 유연성을 제공합니다.

  • PostgreSQL 확장 프로그램: GraphQL 스키마에서 복잡한 유형을 매핑할 필요 없이 설치된 PostgreSQL 확장 프로그램 (예: 지리 공간 데이터용 PostGIS)을 직접 쿼리하고 사용할 수 있습니다.
  • 복잡한 쿼리: 조인, 하위 쿼리, 집계, 윈도우 함수, 저장 프로시저를 사용하여 복잡한 SQL을 실행합니다.
  • 데이터 조작 (DML): INSERT, UPDATE, DELETE 작업을 직접 실행합니다. (하지만 데이터 정의 언어 (DDL) 명령어에는 네이티브 SQL을 사용하지 마세요. 백엔드와 생성된 SDK를 동기화하려면 GraphQL을 사용하여 스키마 수준 변경사항을 계속 적용해야 합니다.)
  • 데이터베이스별 기능: PostgreSQL에 고유한 함수, 연산자 또는 데이터 유형을 활용합니다.
  • 성능 최적화: 중요한 경로에 맞게 SQL 문을 수동으로 조정합니다.

네이티브 SQL 루트 필드

SQL로 작업을 작성하려면 query 또는 mutation 유형의 다음 루트 필드 중 하나를 사용하세요.

필드 query

필드 설명
_select

0개 이상의 행을 반환하는 SQL 쿼리를 실행합니다.

인수:

  • sql: SQL 문 문자열 리터럴입니다. SQL 삽입을 방지하려면 매개변수 값에 위치 자리표시자 ($1, $2 등)를 사용하세요.
  • params: 자리표시자에 바인딩할 값의 순서가 지정된 목록입니다. 여기에는 리터럴, GraphQL 변수, {_expr: "auth.uid"} (인증된 사용자의 ID)와 같은 특수 서버 삽입 컨텍스트 맵이 포함될 수 있습니다.

반환: JSON 배열 ([Any])

_selectFirst

0개 또는 1개의 행을 반환할 것으로 예상되는 SQL 쿼리를 실행합니다.

인수:

  • sql: SQL 문 문자열 리터럴입니다. SQL 삽입을 방지하려면 매개변수 값에 위치 자리표시자 ($1, $2 등)를 사용하세요.
  • params: 자리표시자에 바인딩할 값의 순서가 지정된 목록입니다. 여기에는 리터럴, GraphQL 변수, {_expr: "auth.uid"} (인증된 사용자의 ID)와 같은 특수 서버 삽입 컨텍스트 맵이 포함될 수 있습니다.

반환: JSON 객체 (Any) 또는 null

필드 mutation

필드 설명
_execute

DML 문 (INSERT, UPDATE, DELETE)을 실행합니다.

인수:

  • sql: SQL 문 문자열 리터럴입니다. SQL 삽입을 방지하려면 매개변수 값에 위치 자리표시자 ($1, $2 등)를 사용하세요.

    이 필드는 행 수만 반환하므로 데이터 수정 공통 테이블 표현식 (예: WITH new_row AS (INSERT...))을 사용할 수 있습니다. _execute만 CTE를 지원합니다.

  • params: 자리표시자에 바인딩할 값의 순서가 지정된 목록입니다. 여기에는 리터럴, GraphQL 변수, {_expr: "auth.uid"} (인증된 사용자의 ID)와 같은 특수 서버 삽입 컨텍스트 맵이 포함될 수 있습니다.

반환: Int (영향을 받은 행 수)을 반환합니다.

RETURNING 절은 결과에서 무시됩니다.

_executeReturning

RETURNING 절이 있는 DML 문을 실행하여 0개 이상의 행을 반환합니다.

인수:

  • sql: SQL 문 문자열 리터럴입니다. SQL 삽입을 방지하려면 매개변수 값에 위치 자리표시자 ($1, $2 등)를 사용하세요. 데이터 수정 일반 테이블 표현식은 지원되지 않습니다.
  • params: 자리표시자에 바인딩할 값의 순서가 지정된 목록입니다. 여기에는 리터럴, GraphQL 변수, {_expr: "auth.uid"} (인증된 사용자의 ID)와 같은 특수 서버 삽입 컨텍스트 맵이 포함될 수 있습니다.

반환: JSON 배열 ([Any])

_executeReturningFirst

RETURNING 절이 있는 DML 문을 실행합니다. 0개 또는 1개의 행이 반환될 것으로 예상됩니다.

인수:

  • sql: SQL 문 문자열 리터럴입니다. SQL 삽입을 방지하려면 매개변수 값에 위치 자리표시자 ($1, $2 등)를 사용하세요. 데이터 수정 일반 테이블 표현식은 지원되지 않습니다.
  • params: 자리표시자에 바인딩할 값의 순서가 지정된 목록입니다. 여기에는 리터럴, GraphQL 변수, {_expr: "auth.uid"} (인증된 사용자의 ID)와 같은 특수 서버 삽입 컨텍스트 맵이 포함될 수 있습니다.

반환: JSON 객체 (Any) 또는 null

참고:

  • 작업은 데이터 커넥트 서비스 계정에 부여된 권한을 사용하여 실행됩니다.

  • @table 지시어(@table(name: "ExampleTable"))를 사용하여 테이블 이름을 명시적으로 설정하는 경우 SQL 문 (SELECT field FROM "ExampleTable" ...)에서 테이블 이름을 따옴표로 묶어야 합니다.

    따옴표가 없으면 Data Connect가 테이블 이름을 스네이크 케이스 (example_table)로 변환합니다.

문법 규칙 및 제한사항

네이티브 SQL은 보안을 보장하고 SQL 삽입을 방지하기 위해 엄격한 파싱 규칙을 적용합니다. 다음 제약 조건에 유의하세요.

  • 주석: 블록 주석 (/* ... */)을 사용합니다. 줄 주석 (--)은 쿼리 연결 중에 후속 절 (예: 보안 필터)을 자를 수 있으므로 금지됩니다.
  • 매개변수: params 배열 순서와 일치하는 위치 매개변수 ($1, $2)를 사용합니다. 이름이 지정된 매개변수 ($id, :name)는 지원되지 않습니다.
  • 문자열: 확장 문자열 리터럴 (E'...') 및 달러로 인용된 문자열($$...$$)이 지원됩니다. PostgreSQL 유니코드 이스케이프 (U&'...')는 지원되지 않습니다.

댓글의 매개변수

파서는 블록 주석 내의 모든 항목을 무시합니다. 매개변수 (예: /* WHERE id = $1 */)가 포함된 줄을 주석 처리하는 경우 params 목록에서도 해당 매개변수를 삭제해야 합니다. 그렇지 않으면 unused parameter: $1 오류와 함께 작업이 실패합니다.

예 1: 필드 별칭이 있는 기본 SELECT

루트 필드 (예: movies: _select)에 별칭을 지정하여 클라이언트 응답을 더 깔끔하게 만들 수 있습니다 (data._select 대신 data.movies).

queries.gql:

query GetMoviesByGenre($genre: String!, $limit:Int!) @auth(level: PUBLIC) {
  movies: _select(
    sql: """
      SELECT id, title, release_year, rating
      FROM movie
      WHERE genre = $1
      ORDER BY release_year DESC
      LIMIT $2
    """,
    params: [$genre, $limit]
  )
}

클라이언트 SDK를 사용하여 쿼리를 실행하면 결과가 data.movies에 표시됩니다.

예 2: 기본 UPDATE

mutations.gql:

mutation UpdateMovieRating($movieId: UUID!, $newRating: Float!) @auth(level: NO_ACCESS) {
  _execute(
    sql: """
      UPDATE movie
      SET rating = $2
      WHERE id = $1
    """,
    params: [$movieId, $newRating]
  )
}

클라이언트 SDK를 사용하여 변형을 실행하면 영향을 받는 행 수가 data._execute에 표시됩니다.

예 3: 기본 집계

queries.gql:

query GetTotalReviewCount @auth(level: PUBLIC) {
  stats: _selectFirst(
    sql: "SELECT COUNT(*) as total_reviews FROM \"Reviews\""
  )
}

클라이언트 SDK를 사용하여 쿼리를 실행하면 결과가 data.stats.total_reviews에 표시됩니다.

예 4: RANK를 사용한 고급 집계

queries.gql:

query GetMoviesRankedByRating @auth(level: PUBLIC) {
  _select(
    sql: """
      SELECT
        id,
        title,
        rating,
        RANK() OVER (ORDER BY rating DESC) as rank
      FROM movie
      WHERE rating IS NOT NULL
      LIMIT 20
    """,
    params: []
  )
}

클라이언트 SDK를 사용하여 쿼리를 실행하면 결과가 data._select에 표시됩니다.

예 5: RETURNING 및 인증 컨텍스트를 사용한 UPDATE

mutations.gql:

mutation UpdateMyReviewText($movieId: UUID!, $newText: String!) @auth(level: USER) {
  updatedReview: _executeReturningFirst(
    sql: """
      UPDATE "Reviews"
      SET review_text = $2
      WHERE movie_id = $1 AND user_id = $3
      RETURNING movie_id, user_id, rating, review_text
    """,
    params: [$movieId,$newText,{_expr: "auth.uid" }]
  )
}

클라이언트 SDK를 사용하여 변형을 실행하면 업데이트된 게시물 데이터가 data.updatedReview에 있습니다.

예 6: upsert가 있는 고급 CTE (원자적 get-or-create)

이 패턴은 단일 데이터베이스 트랜잭션에서 하위 레코드 (예: 리뷰)를 삽입하기 전에 종속 레코드 (예: 사용자 또는 영화)가 있는지 확인하는 데 유용합니다.

mutations.gql:

mutation CreateMovieCTE($movieId: UUID!, $userId: UUID!, $reviewId: UUID!) {
  _execute(
    sql: """
      WITH
      new_user AS (
        INSERT INTO "user" (id, username)
        VALUES ($2, 'Auto-Generated User')
        ON CONFLICT (id) DO NOTHING
        RETURNING id
      ),
      movie AS (
        INSERT INTO movie (id, title, image_url, release_year, genre)
        VALUES ($1, 'Auto-Generated Movie', 'https://placeholder.com', 2025, 'Sci-Fi')
        ON CONFLICT (id) DO NOTHING
        RETURNING id
      )
      INSERT INTO "Reviews" (id, movie_id, user_id, rating, review_text, review_date)
      VALUES (
        $3,
        $1,
        $2,
        5,
        'Good!',
        NOW()
      )
    """,
    params: [$movieId, $userId, $reviewId]
  )
}

예 7: Postgres 확장 프로그램 사용

기본 SQL을 사용하면 복잡한 지오메트리 유형을 GraphQL 스키마에 매핑하거나 기본 테이블을 변경하지 않고도 PostGIS와 같은 Postgres 확장 프로그램을 사용할 수 있습니다.

이 예에서는 식당 앱에 메타데이터 JSON 열 (예: {"latitude": 37.3688, "longitude": -122.0363})에 위치 데이터를 저장하는 테이블이 있다고 가정합니다. PostGIS 확장 프로그램을 사용 설정한 경우 표준 Postgres JSON 연산자 (->>)를 사용하여 이러한 값을 즉석에서 추출하고 PostGIS ST_MakePoint 함수에 전달할 수 있습니다.

query GetNearbyActiveRestaurants($userLong: Float!, $userLat: Float!, $maxDistanceMeters: Float!) @auth(level: USER) {
  nearby: _select(
    sql: """
      SELECT
        id,
        name,
        tags,
        ST_Distance(
          ST_MakePoint((metadata->>'longitude')::float, (metadata->>'latitude')::float)::geography,
          ST_MakePoint($1, $2)::geography
        ) as distance_meters
      FROM restaurant
      WHERE active = true
        AND metadata ? 'longitude' AND metadata ? 'latitude'
        AND ST_DWithin(
          ST_MakePoint((metadata->>'longitude')::float, (metadata->>'latitude')::float)::geography,
          ST_MakePoint($1, $2)::geography,
          $3
        )
      ORDER BY distance_meters ASC
      LIMIT 10
    """,
    params: [$userLong, $userLat, $maxDistanceMeters]
  )
}

클라이언트 SDK를 사용하여 쿼리를 실행하면 결과가 data.nearby에 표시됩니다.

보안 권장사항: 동적 SQL 및 저장 프로시저

Data Connect는 GraphQL-데이터베이스 경계에서 모든 입력을 안전하게 매개변수화하여 1차 SQL 삽입으로부터 표준 SQL 쿼리를 완전히 보호합니다. 하지만 SQL을 사용하여 동적 SQL을 실행하는 맞춤 Postgres 저장 프로시저 또는 함수를 호출하는 경우 내부 PL/pgSQL 코드가 이러한 매개변수를 안전하게 처리해야 합니다.

저장 프로시저가 사용자 입력을 EXECUTE 문자열에 직접 연결하면 매개변수화를 우회하고 2차 SQL 삽입 취약점이 생성됩니다.

-- INSECURE: Do not concatenate parameters into dynamic strings!
CREATE OR REPLACE PROCEDURE unsafe_update(user_input TEXT)
LANGUAGE plpgsql AS $$
BEGIN
    -- A malicious user_input (e.g., "val'; DROP TABLE users; --") will execute as code.
    EXECUTE 'UPDATE target_table SET status = ''' || user_input || '''';
END;
$$;

이를 방지하려면 다음 권장사항을 따르세요.

  • USING 절 사용: 저장 프로시저에서 동적 SQL을 작성할 때는 항상 USING 절을 사용하여 데이터 매개변수를 안전하게 바인딩하세요.
  • 식별자에 format() 사용: 안전한 데이터베이스 식별자 삽입 (예: 테이블 이름)을 위해 %I 플래그와 함께 format()을 사용합니다.
  • 식별자 엄격히 허용: 클라이언트 애플리케이션이 데이터베이스 식별자를 임의로 선택하도록 허용하지 마세요. 프로시저에 동적 식별자가 필요한 경우 실행 전에 PL/pgSQL 로직 내에서 하드 코딩된 허용 목록에 대해 입력을 검증합니다.
-- SECURE: Use format() for identifiers and USING for data values
CREATE OR REPLACE PROCEDURE secure_update(target_table TEXT, new_value TEXT, row_id INT)
LANGUAGE plpgsql AS $$
BEGIN
    -- Validate the dynamic table name against an allowlist
    IF target_table NOT IN ('orders', 'users', 'inventory') THEN
        RAISE EXCEPTION 'Invalid table name';
    END IF;

    -- Execute securely
    EXECUTE format('UPDATE %I SET status = $1 WHERE id = $2', target_table)
    USING new_value, row_id;
END;
$$;