뷰(Views)

PostgreSQL
SQLite
MySQL
SingleStore

Drizzle ORM을 사용하면 여러 가지 방법으로 뷰를 선언할 수 있습니다.

데이터베이스에 생성해야 하는 뷰를 선언하거나, 이미 데이터베이스에 존재하는 뷰를 선언할 수 있습니다.

뷰 선언은 쿼리 빌더 문법을 인라인으로 사용하거나, 독립형 쿼리 빌더를 사용하거나, SQL 연산자를 직접 사용하여 할 수 있습니다.

인라인 또는 독립형 쿼리 빌더로 뷰를 생성할 경우, 뷰 컬럼 스키마가 자동으로 추론됩니다. 하지만 SQL을 사용할 때는 뷰 컬럼 스키마를 명시적으로 선언해야 합니다.

뷰 선언하기

PostgreSQL
MySQL
SQLite
schema.ts
import { pgTable, pgView, serial, text, timestamp } from "drizzle-orm/pg-core";

export const user = pgTable("user", {
  id: serial(),
  name: text(),
  email: text(),
  password: text(),
  role: text().$type(),
  createdAt: timestamp("created_at"),
  updatedAt: timestamp("updated_at"),
});

export const userView = pgView("user_view").as((qb) => qb.select().from(user));
export const customersView = pgView("customers_view").as((qb) => qb.select().from(user).where(eq(user.role, "customer")));
CREATE VIEW "user_view" AS SELECT * FROM "user";
CREATE VIEW "customers_view" AS SELECT * FROM "user" WHERE "role" = 'customer';

특정 컬럼만 선택하려면 쿼리 빌더의 .select({ ... }) 메서드를 사용할 수 있습니다. 예를 들면 다음과 같습니다:

export const customersView = pgView("customers_view").as((qb) => {
  return qb
    .select({
      id: user.id,
      name: user.name,
      email: user.email,
    })
    .from(user);
});
CREATE VIEW "customers_view" AS SELECT "id", "name", "email" FROM "user" WHERE "role" = 'customer';

또한, standalone query builder를 사용하여 뷰를 선언할 수도 있습니다. 이 방법은 동일하게 작동합니다:

PostgreSQL
MySQL
SQLite
schema.ts
import { pgTable, pgView, serial, text, timestamp, QueryBuilder} from "drizzle-orm/pg-core";

const qb = new QueryBuilder();

export const user = pgTable("user", {
  id: serial(),
  name: text(),
  email: text(),
  password: text(),
  role: text().$type(),
  createdAt: timestamp("created_at"),
  updatedAt: timestamp("updated_at"),
});

export const userView = pgView("user_view").as(qb.select().from(user));
export const customersView = pgView("customers_view").as(qb.select().from(user).where(eq(user.role, "customer")));
CREATE VIEW "user_view" AS SELECT * FROM "user";
CREATE VIEW "customers_view" AS SELECT * FROM "user" WHERE "role" = 'customer';

Raw SQL로 뷰 선언하기

쿼리 빌더에서 지원하지 않는 문법으로 뷰를 선언해야 할 때, sql 연산자를 직접 사용하고 뷰 컬럼 스키마를 명시적으로 지정할 수 있습니다.

// 일반 뷰
const newYorkers = pgView('new_yorkers', {
  id: serial('id').primaryKey(),
  name: text('name').notNull(),
  cityId: integer('city_id').notNull(),
}).as(sql`select * from ${users} where ${eq(users.cityId, 1)}`);

// 구체화된 뷰(materialized view)
const newYorkers = pgMaterializedView('new_yorkers', {
  id: serial('id').primaryKey(),
  name: text('name').notNull(),
  cityId: integer('city_id').notNull(),
}).as(sql`select * from ${users} where ${eq(users.cityId, 1)}`);

기존 뷰 선언하기

데이터베이스에서 기존 뷰에 대한 읽기 전용 접근 권한이 주어진 경우, .existing() 뷰 설정을 사용해야 합니다. 이렇게 하면 drizzle-kit이 생성된 마이그레이션에서 create view 구문을 무시하고 생성하지 않습니다.

export const user = pgTable("user", {
  id: serial(),
  name: text(),
  email: text(),
  password: text(),
  role: text().$type(),
  createdAt: timestamp("created_at"),
  updatedAt: timestamp("updated_at"),
});

// 일반 뷰
export const trimmedUser = pgView("trimmed_user", {
  id: serial("id"),
  name: text("name"),
  email: text("email"),
}).existing();

// 머티리얼라이즈드 뷰도 동일하게 사용 가능 (일관성을 위해 사용)
export const trimmedUser = pgMaterializedView("trimmed_user", {
  id: serial("id"),
  name: text("name"),
  email: text("email"),
}).existing();

구체화된 뷰(Materialized Views)

PostgreSQL
MySQL
SQLite

공식 문서에 따르면, PostgreSQL은 **일반 뷰**와 **구체화된 뷰**를 모두 지원합니다.

PostgreSQL의 구체화된 뷰는 일반 뷰와 마찬가지로 규칙 시스템을 사용하지만, 결과를 테이블 형태로 저장합니다.

Drizzle ORM은 PostgreSQL의 구체화된 뷰를 기본적으로 지원합니다:

schema.ts
const newYorkers = pgMaterializedView('new_yorkers').as((qb) => qb.select().from(users).where(eq(users.cityId, 1)));
CREATE MATERIALIZED VIEW "new_yorkers" AS SELECT * FROM "users";

애플리케이션 실행 중에 구체화된 뷰를 새로 고칠 수 있습니다:

await db.refreshMaterializedView(newYorkers);

await db.refreshMaterializedView(newYorkers).concurrently();

await db.refreshMaterializedView(newYorkers).withNoData();

확장 예제

쿼리 내부의 모든 매개변수는 $1, $2 등으로 대체되지 않고 그대로 인라인 처리됩니다.

// 일반 뷰
const newYorkers = pgView('new_yorkers')
  .with({
    checkOption: 'cascaded', // 캐스케이드 옵션 적용
    securityBarrier: true, // 보안 장벽 활성화
    securityInvoker: true, // 보안 호출자 활성화
  })
  .as((qb) => {
    const sq = qb
      .$with('sq') // 서브쿼리 정의
      .as(
        qb.select({ userId: users.id, cityId: cities.id })
          .from(users)
          .leftJoin(cities, eq(cities.id, users.homeCity))
          .where(sql`${users.age1} > 18`), // 나이가 18세 이상인 사용자 선택
      );
    return qb.with(sq).select().from(sq).where(sql`${users.homeCity} = 1`); // 홈 도시가 1인 사용자 선택
  });

// 구체화된 뷰
const newYorkers2 = pgMaterializedView('new_yorkers')
  .using('btree') // B-트리 인덱스 사용
  .with({
    fillfactor: 90, // 채우기 비율 90%
    toast_tuple_target: 0.5, // TOAST 튜플 목표치 0.5
    autovacuum_enabled: true, // 자동 청소 활성화
    ...
  })
  .tablespace('custom_tablespace') // 커스텀 테이블스페이스 사용
  .withNoData() // 데이터 없이 생성
  .as((qb) => {
    const sq = qb
      .$with('sq') // 서브쿼리 정의
      .as(
        qb.select({ userId: users.id, cityId: cities.id })
          .from(users)
          .leftJoin(cities, eq(cities.id, users.homeCity))
          .where(sql`${users.age1} > 18`), // 나이가 18세 이상인 사용자 선택
      );
    return qb.with(sq).select().from(sq).where(sql`${users.homeCity} = 1`); // 홈 도시가 1인 사용자 선택
  });