Drizzle 쿼리

PostgreSQL
SQLite
MySQL
SingleStore

Drizzle ORM은 SQL 위에 얇은 타입 레이어로 설계되었습니다. 우리는 TypeScript에서 SQL 데이터베이스를 운영하는 가장 좋은 방법을 설계했다고 확신하며, 이제 더 나아질 때입니다.

관계형 쿼리는 SQL 데이터베이스에서 중첩된 관계형 데이터를 쿼리할 때 여러 조인과 복잡한 데이터 매핑을 피하면서 훌륭한 개발자 경험을 제공하기 위한 것입니다.

이것은 기존 스키마 정의와 쿼리 빌더의 확장입니다. 필요에 따라 선택적으로 사용할 수 있습니다. 우리는 최고 수준의 개발자 경험과 성능을 모두 제공할 수 있도록 했습니다.

index.ts
schema.ts
import * as schema from './schema';
import { drizzle } from 'drizzle-orm/...';

const db = drizzle({ schema });

const result = await db._query.users.findMany({
	with: {
		posts: true			
	},
});
[{
	id: 10,
	name: "Dan",
	posts: [
		{
			id: 1,
			content: "SQL is awesome",
			authorId: 10,
		},
		{
			id: 2,
			content: "But check relational queries",
			authorId: 10,
		}
	]
}]

⚠️ SQL 스키마가 여러 파일에 선언되어 있다면 다음과 같이 할 수 있습니다

index.ts
schema1.ts
schema2.ts
import * as schema1 from './schema1';
import * as schema2 from './schema2';
import { drizzle } from 'drizzle-orm/...';

const db = drizzle({ schema: { ...schema1, ...schema2 } });

const result = await db._query.users.findMany({
	with: {
		posts: true			
	},
});

모드

Drizzle 관계형 쿼리는 데이터베이스에서 실행할 정확히 하나의 SQL 문을 항상 생성하며, 특정한 주의사항이 있습니다. 모든 데이터베이스에 대한 최고 수준의 지원을 제공하기 위해 **모드**를 도입했습니다.

Drizzle 관계형 쿼리는 내부적으로 서브쿼리의 lateral 조인을 사용하며, 현재 PlanetScale은 이를 지원하지 않습니다.

일반 MySQL 데이터베이스에서 mysql2 드라이버를 사용할 때 — mode: "default"를 지정해야 합니다 PlanetScale에서 mysql2 드라이버를 사용할 때 — mode: "planetscale"을 지정해야 합니다

import * as schema from './schema';
import { drizzle } from "drizzle-orm/mysql2";
import mysql from "mysql2/promise";

const connection = await mysql.createConnection({
  uri: process.env.PLANETSCALE_DATABASE_URL,
});

const db = drizzle({ client: connection, schema, mode: 'planetscale' });

쿼리

관계형 쿼리는 Drizzle의 기존 **쿼리 빌더**의 확장입니다. drizzle() 초기화 시 스키마 파일에서 모든 tablesrelations를 제공해야 하며, 그런 다음 db._query API를 사용하면 됩니다.

drizzle import 경로는 사용하는 **데이터베이스 드라이버**에 따라 달라집니다.

index.ts
schema.ts
import * as schema from './schema';
import { drizzle } from 'drizzle-orm/...';

const db = drizzle({ schema });

await db._query.users.findMany(...);
// 여러 파일에 스키마가 있는 경우
import * as schema1 from './schema1';
import * as schema2 from './schema2';
import { drizzle } from 'drizzle-orm/...';

const db = drizzle({ schema: { ...schema1, ...schema2 } });

await db._query.users.findMany(...);

Drizzle은 .findMany().findFirst() API를 제공합니다.

여러 개 찾기

const users = await db._query.users.findMany();
// 결과 타입
const result: {
	id: number;
	name: string;
	verified: boolean;
	invitedBy: number | null;
}[];

첫 번째 찾기

.findFirst()는 쿼리에 limit 1을 추가합니다.

const user = await db._query.users.findFirst();
// 결과 타입
const result: {
	id: number;
	name: string;
	verified: boolean;
	invitedBy: number | null;
};

관계 포함

With 연산자를 사용하면 여러 관련 테이블의 데이터를 결합하고 결과를 적절히 집계할 수 있습니다.

모든 게시글과 댓글 가져오기:

const posts = await db._query.posts.findMany({
	with: {
		comments: true,
	},
});

첫 번째 게시글과 댓글 가져오기:

const post = await db._query.posts.findFirst({
	with: {
		comments: true,
	},
});

필요한 만큼 중첩된 with 문을 체이닝할 수 있습니다. 중첩된 with 쿼리의 경우 Drizzle은 Core Type API를 사용하여 타입을 추론합니다.

모든 사용자와 게시글 가져오기. 각 게시글은 댓글 목록을 포함해야 합니다:

const users = await db._query.users.findMany({
	with: {
		posts: {
			with: {
				comments: true,
			},
		},
	},
});

부분 필드 선택

columns 매개변수를 사용하면 데이터베이스에서 가져올 컬럼을 포함하거나 제외할 수 있습니다.

Drizzle은 쿼리 수준에서 부분 선택을 수행하므로 데이터베이스에서 추가 데이터가 전송되지 않습니다.

Drizzle은 단일 SQL 문을 출력한다는 점을 명심하세요.

id, content만 포함하고 comments를 포함하여 모든 게시글 가져오기:

const posts = await db._query.posts.findMany({
	columns: {
		id: true,
		content: true,
	},
	with: {
		comments: true,
	}
});

content 없이 모든 게시글 가져오기:

const posts = await db._query.posts.findMany({
	columns: {
		content: false,
	},
});

truefalse 선택 옵션이 모두 있으면 모든 false 옵션은 무시됩니다.

name 필드를 포함하고 id 필드를 제외하면, id 제외는 중복됩니다. name을 제외한 모든 필드는 어차피 제외됩니다.

동일한 쿼리에서 필드 제외 및 포함:

const users = await db._query.users.findMany({
	columns: {
		name: true,
		id: false //ignored
	},
});
// 결과 타입
const users: {
	name: string;
};

중첩된 관계의 컬럼만 포함:

const res = await db._query.users.findMany({
	columns: {},
	with: {
		posts: true
	}
});
// 결과 타입
const res: {
	posts: {
		id: number,
		text: string
	}
}[];

중첩 부분 필드 선택

**부분 선택**과 마찬가지로, 중첩된 관계의 컬럼을 포함하거나 제외할 수 있습니다:

const posts = await db._query.posts.findMany({
	columns: {
		id: true,
		content: true,
	},
	with: {
		comments: {
			columns: {
				authorId: false
			}
		}
	}
});

필터 선택

SQL과 유사한 쿼리 빌더와 마찬가지로, 관계형 쿼리 API를 사용하면 연산자 목록으로 필터와 조건을 정의할 수 있습니다.

drizzle-orm에서 import하거나 콜백 구문에서 사용할 수 있습니다:

import { eq } from 'drizzle-orm';

const users = await db._query.users.findMany({
	where: eq(users.id, 1)
})
const users = await db._query.users.findMany({
	where: (users, { eq }) => eq(users.id, 1),
})

id=1인 게시글과 특정 날짜 이전에 생성된 댓글 찾기:

await db._query.posts.findMany({
	where: (posts, { eq }) => (eq(posts.id, 1)),
	with: {
		comments: {
			where: (comments, { lt }) => lt(comments.createdAt, new Date()),
		},
	},
});

Limit & Offset

Drizzle ORM은 쿼리와 중첩된 엔티티에 대한 limitoffset API를 제공합니다.

5개의 게시글 찾기:

await db._query.posts.findMany({
	limit: 5,
});

게시글 찾기 및 댓글 최대 3개 가져오기:

await db._query.posts.findMany({
	with: {
		comments: {
			limit: 3,
		},
	},
});
IMPORTANT

offset은 최상위 쿼리에서만 사용할 수 있습니다.

await db._query.posts.findMany({
	limit: 5,
	offset: 2, // correct ✅
	with: {
		comments: {
			offset: 3, // incorrect ❌
			limit: 3,
		},
	},
});

5번째부터 10번째 게시글까지 댓글과 함께 찾기:

await db._query.posts.findMany({
	limit: 5,
  offset: 5,
	with: {
		comments: true,
	},
});

Order By

Drizzle은 관계형 쿼리 빌더에서 정렬을 위한 API를 제공합니다.

동일한 정렬 **core API**를 사용하거나 import 없이 콜백에서 order by 연산자를 사용할 수 있습니다.

import { desc, asc } from 'drizzle-orm';

await db._query.posts.findMany({
	orderBy: [asc(posts.id)],
});
await db._query.posts.findMany({
	orderBy: (posts, { asc }) => [asc(posts.id)],
});

asc + desc로 정렬:

await db._query.posts.findMany({
	orderBy: (posts, { asc }) => [asc(posts.id)],
	with: {
		comments: {
			orderBy: (comments, { desc }) => [desc(comments.id)],
		},
	},
});

사용자 정의 필드 포함

관계형 쿼리 API를 사용하면 사용자 정의 추가 필드를 추가할 수 있습니다. 데이터를 조회하고 추가 함수를 적용해야 할 때 유용합니다.

IMPORTANT

현재 extras에서 집계는 지원되지 않습니다. 집계가 필요한 경우 **core 쿼리**를 사용하세요.

import { sql } from 'drizzle-orm';

await db._query.users.findMany({
	extras: {
		loweredName: sql`lower(${users.name})`.as('lowered_name'),
	},
})
await db._query.users.findMany({
	extras: {
		loweredName: (users, { sql }) => sql`lower(${users.name})`.as('lowered_name'),
	},
})

lowerName이 키로 반환된 객체의 모든 필드에 포함됩니다.

IMPORTANT

.as("<name_for_column>")을 명시적으로 지정해야 합니다.

모든 사용자를 그룹과 함께 조회하되, fullName 필드를 포함하려면 (firstName과 lastName의 연결), Drizzle 관계형 쿼리 빌더로 다음 쿼리를 사용할 수 있습니다.

const res = await db._query.users.findMany({
	extras: {
		fullName: sql<string>`concat(${users.name}, " ", ${users.name})`.as('full_name'),
	},
	with: {
		usersToGroups: {
			with: {
				group: true,
			},
		},
	},
});
// 결과 타입
const res: {
	id: number;
	name: string;
	verified: boolean;
	invitedBy: number | null;
	fullName: string;
	usersToGroups: {
			group: {
					id: number;
					name: string;
					description: string | null;
			};
	}[];
}[];

모든 게시글과 댓글을 조회하고 게시글 내용의 크기와 각 댓글 내용의 크기를 계산하는 추가 필드를 추가하려면:

const res = await db._query.posts.findMany({
	extras: (table, { sql }) => ({
		contentLength: (sql<number>`length(${table.content})`).as('content_length'),
	}),
	with: {
		comments: {
			extras: {
				commentSize: sql<number>`length(${comments.content})`.as('comment_size'),
			},
		},
	},
});
// 결과 타입
const res: {
	id: number;
	createdAt: Date;
	content: string;
	authorId: number | null;
	contentLength: number;
	comments: {
			id: number;
			createdAt: Date;
			content: string;
			creator: number | null;
			postId: number | null;
			commentSize: number;
	}[];
};

Prepared 문

Prepared 문은 쿼리 성능을 대폭 향상시키도록 설계되었습니다 — 여기를 참조하세요.

이 섹션에서는 Drizzle 관계형 쿼리 빌더를 사용하여 플레이스홀더를 정의하고 prepared 문을 실행하는 방법을 배울 수 있습니다.

where에서 플레이스홀더
PostgreSQL
MySQL
SQLite
const prepared = db._query.users.findMany({
	where: ((users, { eq }) => eq(users.id, placeholder('id'))),
	with: {
		posts: {
			where: ((users, { eq }) => eq(users.id, placeholder('pid'))),
		},
	},
}).prepare('query_name');

const usersWithPosts = await prepared.execute({ id: 1 });
limit에서 플레이스홀더
PostgreSQL
MySQL
SQLite
const prepared = db._query.users.findMany({
	with: {
		posts: {
			limit: placeholder('limit'),
		},
	},
}).prepare('query_name');

const usersWithPosts = await prepared.execute({ limit: 1 });
offset에서 플레이스홀더
PostgreSQL
MySQL
SQLite
const prepared = db._query.users.findMany({
	offset: placeholder('offset'),
	with: {
		posts: true,
	},
}).prepare('query_name');

const usersWithPosts = await prepared.execute({ offset: 1 });
여러 플레이스홀더
PostgreSQL
MySQL
SQLite
const prepared = db._query.users.findMany({
	limit: placeholder('uLimit'),
	offset: placeholder('uOffset'),
	where: ((users, { eq, or }) => or(eq(users.id, placeholder('id')), eq(users.id, 3))),
	with: {
		posts: {
			where: ((users, { eq }) => eq(users.id, placeholder('pid'))),
			limit: placeholder('pLimit'),
		},
	},
}).prepare('query_name');

const usersWithPosts = await prepared.execute({ pLimit: 1, uLimit: 3, uOffset: 1, id: 2, pid: 6 });