PostgreSQL 결과 검증 및 쿼리 정적 유형 유추

11183 단어 nodepostgres
원래 내Contra 프로필에 게시되었습니다.

대부분의 버그는 잘못된 기대에서 비롯됩니다. 데이터베이스로 작업할 때 첫 번째 방어선은 예상되는 쿼리 결과를 정적으로 입력하는 것입니다.

type SubscriptionPayment = {
  id: number,
  subscriber: string,
  amount: number
};

await connection.many(
  sql<Person>`
    SELECT id, subscriber, amount
    FROM subscription_payment
  `
);


이렇게 하면 쿼리를 보지 않고도 쿼리 결과에 대해 가정할 수 있으므로 개발자 경험이 이미 크게 향상됩니다.

쿼리가 예상 결과를 반환하는지 추가로 검증하는 통합 테스트가 있을 수도 있습니다(최소한 빌드 시간에).

그러나 문제는 일단 애플리케이션을 배포하면 데이터베이스 스키마가 코드베이스와 독립적으로 변경될 수 있다는 것입니다. 이 드리프트로 인해 애플리케이션이 예측할 수 없고 잠재적으로 위험한 방식으로 작동할 수 있습니다. 예를 들어 금액 열 유형이 숫자에서 텍스트로 변경된 경우를 상상해 보십시오.

런타임 유효성 검사가 없으면 일련의 문제와 잠재적인 데이터베이스 손상이 발생할 수 있습니다. 더 나쁜 것은 런타임 검사가 없으면 오랫동안 알아차리지 못할 수 있습니다.

반대로 런타임 검사를 사용하면 코드베이스와 데이터베이스 간의 계약이 항상 준수되는지 확인할 수 있습니다. 주요 변경 사항이 있는 경우 디버그 및 수정하기 쉬운 큰 오류와 함께 응용 프로그램이 실패합니다.
지금까지는 데이터베이스 쿼리에 런타임 유효성 검사를 추가하는 간단한 방법이 없었습니다.

그러나 스키마 유효성 검사 및 정적 유형 간섭 라이브러리인 zodSlonik 덕분에 이제 쿼리당 단일 스키마를 작성하고 정적 유형 및 런타임 결과 유효성 검사를 가져옴으로써 두 가지 장점을 모두 누릴 수 있습니다.

JavaScript 생태계에 익숙하다면 Relay가 React.js/GraphQL이라면 Slonik은 Node.js/PostgreSQL입니다.
작동 방식은 다음과 같습니다.

PostgreSQL 테이블 사람이 있다고 가정해 보겠습니다.

CREATE TABLE "public"."person"(
  "id" integer GENERATED ALWAYS AS IDENTITY,
  "name" text NOT NULL,
  PRIMARY KEY ("id")
);


ID 및 이름과 함께 데이터베이스의 모든 사람을 검색하려고 합니다.

connection.any(sql`
  SELECT id, name
  FROM person
`);


데이터베이스 스키마에 대한 지식을 바탕으로 zod 개체를 정의합니다.

const personObject = z.object({
  id: z.number(),
  name: z.string(),
});


sql.type 태그를 사용하고 personObject를 전달하도록 쿼리를 업데이트합니다.

const personQuery = sql.type(personObject)`
  SELECT id, name
  FROM person
`;


마지막으로 typed sql 태그 템플릿을 사용하여 데이터베이스를 쿼리합니다.

const persons = await connection.any(personQuery);


이 정보를 통해 Slonik은 people의 모든 구성원이 각각 null이 아닌 숫자와 null이 아닌 문자열인 id 및 name 속성을 갖는 객체임을 보장합니다.

스키마 유효성 검사 오류 처리



쿼리가 zod 개체를 충족하지 않는 행을 생성하면 SchemaValidationError 오류가 발생합니다.SchemaValidationError에는 쿼리 및 유효성 검사 오류를 설명하는 속성이 포함되어 있습니다.
  • sql – 예기치 않은 행을 생성한 쿼리의 SQL입니다.
  • row - 스키마를 충족하지 않는 행 데이터입니다.
  • issues – 충족되지 않은 기대치의 배열입니다.

  • 이 오류가 발생할 때마다 동일한 정보가 로그에도 포함됩니다.

    대부분의 경우 개별 쿼리 수준에서 이러한 오류를 처리하려고 시도해서는 안 됩니다. 응용 프로그램의 맨 위로 전파되도록 허용하고 문제를 인식할 때 문제를 수정합니다.

    그러나 구조화되지 않은 데이터를 처리하는 경우와 같은 경우 쿼리 수준에서 이러한 오류를 처리하는 것이 유용할 수 있습니다.

    import { SchemaValidationError } from 'slonik';
    
    try {} catch(error) {
      if (errorextendsSchemaValidationError) {
        // Handle scheme validation error
      }
    }
    


    성능 저하



    네트워크 오버헤드의 맥락에서 유효성 검사는 총 실행 시간의 작은 부분을 차지합니다. 아이디어를 제공하기 위해 데이터 샘플에서 1개의 행을 유효성 검사하는 데 0.1ms 미만, 1,000개의 유효성을 검사하는 데 ~3ms, ~25ms가 걸립니다. 100,000개의 행을 검증합니다.

    알 수 없는 키



    Slonik은 알 수 없는 키를 허용하지 않습니다. 즉, {foo: 'bar', baz: 'qux'} 스키마와 함께 z.object({foo: z.string()})를 반환하는 쿼리는 SchemaValidationError 오류를 생성합니다.

    유추 유형



    쿼리 결과의 TypeScript 유형을 유추할 수 있습니다. 몇 가지 방법이 있습니다.

    // Infer using z.infer<typeof yourSchema>
    // https://github.com/colinhacks/zod#type-inference
    type Person = z.infer<typeof personObject>;
    
    // from sql tagged template `zodObject` property 
    type Person = z.infer<personQuery.zodObject>;
    


    결과 변환



    zod 변환을 사용하면 결과 모양과 유형을 구체화할 수 있습니다.

    const coordinatesType = z.string().transform((subject) => {  
      const [x,y] = subject.split(',');
      return{x:Number(x),y:Number(y)};
    });
    
    const zodObject = z.object({foo:coordinatesType});
    const query = sql.type(zodObject)`SELECT '1,2' as foo`;
    const result = await pool.one(query);
    expectTypeOf(result).toMatchTypeOf<{foo:{x:number,y:number}}>();
    
    t.deepEqual(result,{foo:{x:1,y:2}});
    


    무엇 향후 계획?



    쿼리 계측을 위한 프레임워크가 있으므로 다음 단계는 가능한 한 스키마 및 유형 생성을 자동화하는 것입니다. 이를 위해 mmkal이 수행한 작업https://www.npmjs.com/package/@slonik/typegen을 확인하십시오.

    좋은 웹페이지 즐겨찾기