Drizzle 쿼리
Drizzle ORM은 SQL 위에 얇은 타입 계층을 제공하도록 설계되었습니다.
우리는 TypeScript에서 SQL 데이터베이스를 다루는 최고의 방법을 설계했다고 믿으며, 이를 더욱 개선할 때가 왔다고 생각합니다.
관계형 쿼리는 여러 조인과 복잡한 데이터 매핑을 피하면서, SQL 데이터베이스에서 중첩된 관계형 데이터를 쿼리할 수 있는 뛰어난 개발자 경험을 제공합니다.
이는 기존 스키마 정의와 쿼리 빌더에 대한 확장입니다.
여러분의 필요에 따라 선택적으로 사용할 수 있습니다.
우리는 최고 수준의 개발자 경험과 성능을 모두 제공하도록 설계했습니다.
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 ,
}
]
}]
import { integer , serial , text , pgTable } from 'drizzle-orm/pg-core' ;
import { relations } from 'drizzle-orm' ;
export const users = pgTable ( 'users' , {
id : serial ( 'id' ) .primaryKey () ,
name : text ( 'name' ) .notNull () ,
});
export const usersRelations = relations (users , ({ many }) => ({
posts : many (posts) ,
}));
export const posts = pgTable ( 'posts' , {
id : serial ( 'id' ) .primaryKey () ,
content : text ( 'content' ) .notNull () ,
authorId : integer ( 'author_id' ) .notNull () ,
});
export const postsRelations = relations (posts , ({ one }) => ({
author : one (users , { fields : [ posts .authorId] , references : [ users .id] }) ,
}));
⚠️ SQL 스키마가 여러 파일에 선언된 경우 다음과 같이 처리할 수 있습니다.
index.ts
schema1.ts
schema2.ts
import * as schema 1 from './ schema 1' ;
import * as schema 2 from './ schema 2' ;
import { drizzle } from 'drizzle-orm/...' ;
const db = drizzle ({ schema : { ... schema 1 , ... schema 2 } });
const result = await db . query . users .findMany ({
with : {
posts : true
} ,
});
// 첫 번째 파일의 스키마 선언
// 두 번째 파일의 스키마 선언
모드
Drizzle의 관계형 쿼리는 항상 데이터베이스에서 실행할 하나의 SQL 문을 생성하며, 여기에는 몇 가지 주의사항이 있습니다. 모든 데이터베이스에 최고 수준의 지원을 제공하기 위해 **모드(mode)
**를 도입했습니다.
Drizzle의 관계형 쿼리는 내부적으로 서브쿼리의 lateral join 을 사용합니다. 현재 PlanetScale은 이를 지원하지 않습니다.
mysql2 드라이버를 일반 MySQL 데이터베이스와 함께 사용할 때는 mode: "default"
를 지정해야 합니다.
mysql2 드라이버를 PlanetScale 과 함께 사용할 때는 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' });
쿼리(Querying)
관계형 쿼리는 Drizzle의 기존 **쿼리 빌더 **를 확장한 기능입니다. drizzle()
초기화 시 스키마 파일에서 모든 tables
와 relations
를 제공한 후, db.query
API를 사용하면 됩니다.
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 ( ... );
import { type AnyPgColumn , boolean , integer , pgTable , primaryKey , serial , text , timestamp } from 'drizzle-orm/pg-core' ;
import { relations } from 'drizzle-orm' ;
export const users = pgTable ( 'users' , {
id : serial ( 'id' ) .primaryKey () ,
name : text ( 'name' ) .notNull () ,
invitedBy : integer ( 'invited_by' ) .references (() : AnyPgColumn => users .id) ,
});
export const usersRelations = relations (users , ({ one , many }) => ({
invitee : one (users , { fields : [ users .invitedBy] , references : [ users .id] }) ,
usersToGroups : many (usersToGroups) ,
posts : many (posts) ,
}));
export const groups = pgTable ( 'groups' , {
id : serial ( 'id' ) .primaryKey () ,
name : text ( 'name' ) .notNull () ,
description : text ( 'description' ) ,
});
export const groupsRelations = relations (groups , ({ many }) => ({
usersToGroups : many (usersToGroups) ,
}));
export const usersToGroups = pgTable ( 'users_to_groups' , {
id : serial ( 'id' ) .primaryKey () ,
userId : integer ( 'user_id' ) .notNull () .references (() => users .id) ,
groupId : integer ( 'group_id' ) .notNull () .references (() => groups .id) ,
} , (t) => ({
pk : primaryKey ( t .userId , t .groupId) ,
}));
export const usersToGroupsRelations = relations (usersToGroups , ({ one }) => ({
group : one (groups , { fields : [ usersToGroups .groupId] , references : [ groups .id] }) ,
user : one (users , { fields : [ usersToGroups .userId] , references : [ users .id] }) ,
}));
export const posts = pgTable ( 'posts' , {
id : serial ( 'id' ) .primaryKey () ,
content : text ( 'content' ) .notNull () ,
authorId : integer ( 'author_id' ) .references (() => users .id) ,
createdAt : timestamp ( 'created_at' , { withTimezone : true }) .notNull () .defaultNow () ,
});
export const postsRelations = relations (posts , ({ one , many }) => ({
author : one (users , { fields : [ posts .authorId] , references : [ users .id] }) ,
comments : many (comments) ,
}));
export const comments = pgTable ( 'comments' , {
id : serial ( 'id' ) .primaryKey () ,
content : text ( 'content' ) .notNull () ,
creator : integer ( 'creator' ) .references (() => users .id) ,
postId : integer ( 'post_id' ) .references (() => posts .id) ,
createdAt : timestamp ( 'created_at' , { withTimezone : true }) .notNull () .defaultNow () ,
});
export const commentsRelations = relations (comments , ({ one , many }) => ({
post : one (posts , { fields : [ comments .postId] , references : [ posts .id] }) ,
author : one (users , { fields : [ comments .creator] , references : [ users .id] }) ,
likes : many (commentLikes) ,
}));
export const commentLikes = pgTable ( 'comment_likes' , {
id : serial ( 'id' ) .primaryKey () ,
creator : integer ( 'creator' ) .references (() => users .id) ,
commentId : integer ( 'comment_id' ) .references (() => comments .id) ,
createdAt : timestamp ( 'created_at' , { withTimezone : true }) .notNull () .defaultNow () ,
});
export const commentLikesRelations = relations (commentLikes , ({ one }) => ({
comment : one (comments , { fields : [ commentLikes .commentId] , references : [ comments .id] }) ,
author : one (users , { fields : [ commentLikes .creator] , references : [ users .id] }) ,
}));
Drizzle는 .findMany()
와 .findFirst()
API를 제공합니다.
여러 항목 조회하기
const users = await db . query . users .findMany ();
// 결과 타입
const result : {
id : number ;
name : string ;
verified : boolean ;
invitedBy : number | null ;
}[];
위 코드는 데이터베이스에서 users
테이블의 여러 항목을 조회하는 예제입니다. findMany()
함수를 사용하면 해당 테이블의 모든 사용자 정보를 배열 형태로 가져올 수 있습니다. 결과 타입은 각 사용자의 id
, name
, verified
, invitedBy
속성을 포함하는 객체의 배열입니다.
첫 번째 항목 찾기
.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
문을 체이닝할 수 있습니다.
Drizzle는 Core Type API 를 사용하여 모든 중첩된 with
쿼리의 타입을 추론합니다.
게시물과 댓글 목록이 포함된 모든 사용자 가져오기:
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 ,
} ,
});
true
와 false
선택 옵션이 모두 존재할 경우, 모든 false
옵션은 무시됩니다.
name
필드를 포함하고 id
필드를 제외하면, id
제외는 불필요해집니다.
name
을 제외한 모든 필드는 어쨌든 제외될 것이기 때문입니다.
동일한 쿼리에서 필드 포함 및 제외하기:
const users = await db . query . users .findMany ({
columns : {
name : true ,
id : false // 무시됨
} ,
});
// 결과 타입
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
}
}
}
});
이 예제에서는 posts
테이블에서 id
와 content
컬럼만 선택하고, comments
관계에서 authorId
컬럼은 제외합니다.
필터 선택하기
SQL과 유사한 쿼리 빌더에서와 마찬가지로, 관계형 쿼리 API를 사용하면 operators
목록을 통해 필터와 조건을 정의할 수 있습니다.
drizzle-orm
에서 직접 불러오거나, 콜백 문법을 사용할 수 있습니다:
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은 쿼리와 중첩된 엔티티에 대해 limit
과 offset
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 , // 올바름 ✅
with : {
comments : {
offset : 3 , // 잘못됨 ❌
limit : 3 ,
} ,
} ,
});
5번째부터 10번째 게시물과 댓글 조회:
await db . query . posts .findMany ({
limit : 5 ,
offset : 5 ,
with : {
comments : true ,
} ,
});
Order By (정렬)
Drizzle은 관계형 쿼리 빌더에서 정렬을 위한 API를 제공합니다.
여러분은 동일한 **코어 API **를 사용하거나, 콜백 내에서 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
에서는 집계(aggregation)를 지원하지 않습니다. 이 기능을 사용하려면 **core queries
**를 사용하세요.
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("<컬럼_이름>")
을 명시적으로 지정해야 합니다.
firstName
과 lastName
을 연결한 fullName
필드를 포함하여 모든 사용자와 그룹을 가져오려면, Drizzle 관계형 쿼리 빌더를 사용하여 다음과 같이 쿼리할 수 있습니다.
const res = await db . query . users .findMany ({
extras : {
fullName : sql `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 `length( ${ table .content } )` ) .as ( 'content_length' ) ,
}) ,
with : {
comments : {
extras : {
commentSize : sql `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 Statements (준비된 문장)
Prepared Statements는 쿼리 성능을 크게 향상시키기 위해 설계되었습니다. 여기에서 자세히 알아보세요.
이 섹션에서는 Drizzle 관계형 쿼리 빌더를 사용하여 플레이스홀더를 정의하고 Prepared Statements를 실행하는 방법을 배울 수 있습니다.
where
절에서의 플레이스홀더 사용
const prepared = db . query . users .findMany ({
where : ((users , { eq }) => eq ( users .id , placeholder ( 'id' ))) ,
with : {
posts : {
where : ((users , { eq }) => eq ( users .id , 1 )) ,
} ,
} ,
}) .prepare ( 'query_name' );
const usersWithPosts = await prepared .execute ({ id : 1 });
const prepared = db . query . users .findMany ({
where : ((users , { eq }) => eq ( users .id , placeholder ( 'id' ))) ,
with : {
posts : {
where : ((users , { eq }) => eq ( users .id , 1 )) ,
} ,
} ,
}) .prepare ();
const usersWithPosts = await prepared .execute ({ id : 1 });
const prepared = db . query . users .findMany ({
where : ((users , { eq }) => eq ( users .id , placeholder ( 'id' ))) ,
with : {
posts : {
where : ((users , { eq }) => eq ( users .id , 1 )) ,
} ,
} ,
}) .prepare ();
const usersWithPosts = await prepared .execute ({ id : 1 });
위 코드는 데이터베이스 쿼리에서 where
절에 플레이스홀더를 사용하는 예제입니다. 각 탭은 PostgreSQL, MySQL, SQLite에 따라 약간의 차이가 있습니다.
PostgreSQL : prepare
메서드에 쿼리 이름을 지정할 수 있습니다.
MySQL 및 SQLite : prepare
메서드에 쿼리 이름을 지정하지 않습니다.
플레이스홀더는 placeholder('id')
와 같이 사용되며, 실행 시 execute
메서드에 전달된 객체에서 해당 값을 가져옵니다. 이 예제에서는 id
값이 1
인 사용자와 그 사용자의 게시물을 조회합니다.
limit
에 플레이스홀더 사용하기
const prepared = db . query . users .findMany ({
with : {
posts : {
limit : placeholder ( 'limit' ) ,
} ,
} ,
}) .prepare ( 'query_name' );
const usersWithPosts = await prepared .execute ({ limit : 1 });
const prepared = db . query . users .findMany ({
with : {
posts : {
limit : placeholder ( 'limit' ) ,
} ,
} ,
}) .prepare ();
const usersWithPosts = await prepared .execute ({ limit : 1 });
const prepared = db . query . users .findMany ({
with : {
posts : {
limit : placeholder ( 'limit' ) ,
} ,
} ,
}) .prepare ();
const usersWithPosts = await prepared .execute ({ limit : 1 });
위 예제는 limit
에 플레이스홀더를 사용하는 방법을 보여줍니다. placeholder('limit')
를 통해 limit
값을 동적으로 설정할 수 있습니다. prepare()
메서드를 사용해 쿼리를 준비한 후, execute()
메서드에 limit
값을 전달하여 실행합니다. 이 방식은 PostgreSQL, MySQL, SQLite 모두에서 동일하게 적용됩니다.
offset
에서의 플레이스홀더 사용
const prepared = db . query . users .findMany ({
offset : placeholder ( 'offset' ) ,
with : {
posts : true ,
} ,
}) .prepare ( 'query_name' );
const usersWithPosts = await prepared .execute ({ offset : 1 });
const prepared = db . query . users .findMany ({
offset : placeholder ( 'offset' ) ,
with : {
posts : true ,
} ,
}) .prepare ();
const usersWithPosts = await prepared .execute ({ offset : 1 });
const prepared = db . query . users .findMany ({
offset : placeholder ( 'offset' ) ,
with : {
posts : true ,
} ,
}) .prepare ();
const usersWithPosts = await prepared .execute ({ offset : 1 });
여러 개의 플레이스홀더 사용하기
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 });
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 ();
const usersWithPosts = await prepared .execute ({ pLimit : 1 , uLimit : 3 , uOffset : 1 , id : 2 , pid : 6 });
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 ();
const usersWithPosts = await prepared .execute ({ pLimit : 1 , uLimit : 3 , uOffset : 1 , id : 2 , pid : 6 });