Files
gh-neondatabase-labs-ai-rul…/skills/neon-drizzle/references/query-patterns.md
2025-11-30 08:43:11 +08:00

762 lines
14 KiB
Markdown

# Query Patterns Reference Guide
Complete reference for querying with Drizzle ORM.
## Table of Contents
- [Basic CRUD Operations](#basic-crud-operations)
- [Advanced Filtering](#advanced-filtering)
- [Joins and Relations](#joins-and-relations)
- [Aggregations](#aggregations)
- [Subqueries](#subqueries)
- [Transactions](#transactions)
- [Batch Operations](#batch-operations)
- [Raw SQL](#raw-sql)
- [Performance Optimization](#performance-optimization)
- [Type Safety](#type-safety)
- [Common Patterns](#common-patterns)
- [Related Resources](#related-resources)
---
## Basic CRUD Operations
### Create (Insert)
**Single record:**
```typescript
import { db } from './db';
import { users } from './db/schema';
const newUser = await db.insert(users)
.values({
email: 'user@example.com',
name: 'John Doe',
})
.returning();
console.log(newUser[0]); // { id: 1, email: '...', name: '...' }
```
**Multiple records:**
```typescript
const newUsers = await db.insert(users)
.values([
{ email: 'user1@example.com', name: 'User 1' },
{ email: 'user2@example.com', name: 'User 2' },
{ email: 'user3@example.com', name: 'User 3' },
])
.returning();
```
**With onConflictDoNothing:**
```typescript
await db.insert(users)
.values({ email: 'user@example.com', name: 'John' })
.onConflictDoNothing();
```
**With onConflictDoUpdate (upsert):**
```typescript
await db.insert(users)
.values({ email: 'user@example.com', name: 'John' })
.onConflictDoUpdate({
target: users.email,
set: { name: 'John Updated' },
});
```
### Read (Select)
**All records:**
```typescript
const allUsers = await db.select().from(users);
```
**Specific columns:**
```typescript
const userEmails = await db.select({
id: users.id,
email: users.email,
}).from(users);
```
**With WHERE clause:**
```typescript
import { eq, gt, lt, like, and, or } from 'drizzle-orm';
const user = await db.select()
.from(users)
.where(eq(users.email, 'user@example.com'));
const activeUsers = await db.select()
.from(users)
.where(eq(users.isActive, true));
```
**Multiple conditions:**
```typescript
const filteredUsers = await db.select()
.from(users)
.where(
and(
eq(users.isActive, true),
gt(users.createdAt, new Date('2024-01-01'))
)
);
```
**With LIMIT and OFFSET:**
```typescript
const paginatedUsers = await db.select()
.from(users)
.limit(10)
.offset(20); // Page 3
```
**With ORDER BY:**
```typescript
const sortedUsers = await db.select()
.from(users)
.orderBy(users.createdAt); // ASC by default
import { desc } from 'drizzle-orm';
const recentUsers = await db.select()
.from(users)
.orderBy(desc(users.createdAt));
```
### Update
**Single record:**
```typescript
await db.update(users)
.set({ name: 'Jane Doe' })
.where(eq(users.id, 1));
```
**Multiple records:**
```typescript
await db.update(users)
.set({ isActive: false })
.where(eq(users.deletedAt, null));
```
**With returning:**
```typescript
const updated = await db.update(users)
.set({ name: 'Updated Name' })
.where(eq(users.id, 1))
.returning();
```
**Partial updates:**
```typescript
const updates: Partial<typeof users.$inferSelect> = {
name: 'New Name',
};
await db.update(users)
.set(updates)
.where(eq(users.id, 1));
```
### Delete
**Single record:**
```typescript
await db.delete(users)
.where(eq(users.id, 1));
```
**Multiple records:**
```typescript
await db.delete(users)
.where(eq(users.isActive, false));
```
**With returning:**
```typescript
const deleted = await db.delete(users)
.where(eq(users.id, 1))
.returning();
```
**Soft delete (recommended):**
```typescript
await db.update(users)
.set({ deletedAt: new Date() })
.where(eq(users.id, 1));
```
## Advanced Filtering
### Comparison Operators
```typescript
import { eq, ne, gt, gte, lt, lte } from 'drizzle-orm';
const adults = await db.select()
.from(users)
.where(gte(users.age, 18));
const recentPosts = await db.select()
.from(posts)
.where(gt(posts.createdAt, new Date('2024-01-01')));
const excludeAdmin = await db.select()
.from(users)
.where(ne(users.role, 'admin'));
```
### Pattern Matching
```typescript
import { like, ilike } from 'drizzle-orm';
const gmailUsers = await db.select()
.from(users)
.where(like(users.email, '%@gmail.com'));
const searchByName = await db.select()
.from(users)
.where(ilike(users.name, '%john%')); // Case-insensitive
```
### NULL Checks
```typescript
import { isNull, isNotNull } from 'drizzle-orm';
const usersWithPhone = await db.select()
.from(users)
.where(isNotNull(users.phoneNumber));
const unverifiedUsers = await db.select()
.from(users)
.where(isNull(users.emailVerifiedAt));
```
### IN Operator
```typescript
import { inArray } from 'drizzle-orm';
const specificUsers = await db.select()
.from(users)
.where(inArray(users.id, [1, 2, 3, 4, 5]));
```
### BETWEEN
```typescript
import { between } from 'drizzle-orm';
const postsThisMonth = await db.select()
.from(posts)
.where(
between(
posts.createdAt,
new Date('2024-01-01'),
new Date('2024-01-31')
)
);
```
### Complex Conditions
```typescript
import { and, or, not } from 'drizzle-orm';
const complexQuery = await db.select()
.from(users)
.where(
or(
and(
eq(users.isActive, true),
gte(users.age, 18)
),
eq(users.role, 'admin')
)
);
```
## Joins and Relations
### Manual Joins
**Inner join:**
```typescript
const postsWithAuthors = await db.select({
postId: posts.id,
postTitle: posts.title,
authorName: users.name,
authorEmail: users.email,
})
.from(posts)
.innerJoin(users, eq(posts.authorId, users.id));
```
**Left join:**
```typescript
const allPostsWithOptionalAuthors = await db.select()
.from(posts)
.leftJoin(users, eq(posts.authorId, users.id));
```
### Relational Queries (Recommended)
**Define relations first:**
```typescript
import { relations } from 'drizzle-orm';
export const usersRelations = relations(users, ({ many }) => ({
posts: many(posts),
}));
export const postsRelations = relations(posts, ({ one }) => ({
author: one(users, {
fields: [posts.authorId],
references: [users.id],
}),
}));
```
**Query with relations:**
```typescript
const usersWithPosts = await db.query.users.findMany({
with: {
posts: true,
},
});
console.log(usersWithPosts[0].posts); // Array of posts
```
**Nested relations:**
```typescript
const postsWithAuthorsAndComments = await db.query.posts.findMany({
with: {
author: true,
comments: {
with: {
author: true,
},
},
},
});
```
**Filtered relations:**
```typescript
const usersWithRecentPosts = await db.query.users.findMany({
with: {
posts: {
where: gt(posts.createdAt, new Date('2024-01-01')),
orderBy: desc(posts.createdAt),
limit: 5,
},
},
});
```
**Partial selection:**
```typescript
const usersWithPostTitles = await db.query.users.findMany({
columns: {
id: true,
name: true,
},
with: {
posts: {
columns: {
id: true,
title: true,
},
},
},
});
```
## Aggregations
### Count
```typescript
import { count } from 'drizzle-orm';
const userCount = await db.select({
count: count(),
}).from(users);
console.log(userCount[0].count); // Total users
```
**Count with grouping:**
```typescript
const postsByAuthor = await db.select({
authorId: posts.authorId,
postCount: count(),
})
.from(posts)
.groupBy(posts.authorId);
```
### Sum, Avg, Min, Max
```typescript
import { sum, avg, min, max } from 'drizzle-orm';
const stats = await db.select({
totalViews: sum(posts.views),
avgViews: avg(posts.views),
minViews: min(posts.views),
maxViews: max(posts.views),
}).from(posts);
```
### Having
```typescript
const activeAuthors = await db.select({
authorId: posts.authorId,
postCount: count(),
})
.from(posts)
.groupBy(posts.authorId)
.having(gt(count(), 5)); // Authors with > 5 posts
```
## Subqueries
### In WHERE clause
```typescript
const activeUserIds = db.select({ id: users.id })
.from(users)
.where(eq(users.isActive, true));
const postsFromActiveUsers = await db.select()
.from(posts)
.where(inArray(posts.authorId, activeUserIds));
```
### As derived table
```typescript
const recentPosts = db.select()
.from(posts)
.where(gt(posts.createdAt, new Date('2024-01-01')))
.as('recentPosts');
const authorsOfRecentPosts = await db.select()
.from(users)
.innerJoin(recentPosts, eq(users.id, recentPosts.authorId));
```
## Transactions
**Only available with WebSocket adapter!**
```typescript
await db.transaction(async (tx) => {
const user = await tx.insert(users)
.values({ email: 'user@example.com', name: 'John' })
.returning();
await tx.insert(posts)
.values({
authorId: user[0].id,
title: 'First post',
content: 'Hello world',
});
});
```
**With error handling:**
```typescript
try {
await db.transaction(async (tx) => {
await tx.insert(users).values({ email: 'user@example.com' });
await tx.insert(posts).values({ title: 'Post' });
throw new Error('Rollback!'); // Transaction rolls back
});
} catch (err) {
console.error('Transaction failed:', err);
}
```
**Nested transactions:**
```typescript
await db.transaction(async (tx) => {
await tx.insert(users).values({ email: 'user1@example.com' });
await tx.transaction(async (tx2) => {
await tx2.insert(posts).values({ title: 'Post 1' });
});
});
```
## Batch Operations
**HTTP adapter alternative to transactions:**
```typescript
await db.batch([
db.insert(users).values({ email: 'user1@example.com' }),
db.insert(users).values({ email: 'user2@example.com' }),
db.insert(posts).values({ title: 'Post 1' }),
]);
```
**Note:** Not atomic! Use transactions if you need rollback capability.
## Raw SQL
### Execute raw query
```typescript
import { sql } from 'drizzle-orm';
const result = await db.execute(sql`
SELECT * FROM users
WHERE email LIKE ${'%@gmail.com'}
`);
```
### SQL in WHERE clause
```typescript
const users = await db.select()
.from(users)
.where(sql`${users.email} LIKE '%@gmail.com'`);
```
### SQL expressions
```typescript
const posts = await db.select({
id: posts.id,
title: posts.title,
excerpt: sql<string>`LEFT(${posts.content}, 100)`,
}).from(posts);
```
### Custom functions
```typescript
const searchResults = await db.select()
.from(posts)
.where(
sql`to_tsvector('english', ${posts.content}) @@ to_tsquery('english', ${'search query'})`
);
```
## Performance Optimization
### Select only needed columns
**Bad:**
```typescript
const users = await db.select().from(users); // All columns
```
**Good:**
```typescript
const users = await db.select({
id: users.id,
email: users.email,
}).from(users);
```
### Use indexes
**Ensure indexed columns in WHERE:**
```typescript
// Assuming index on users.email
const user = await db.select()
.from(users)
.where(eq(users.email, 'user@example.com')); // Fast
```
### Avoid N+1 queries
**Bad:**
```typescript
const posts = await db.select().from(posts);
for (const post of posts) {
const author = await db.select()
.from(users)
.where(eq(users.id, post.authorId)); // N queries!
}
```
**Good:**
```typescript
const posts = await db.query.posts.findMany({
with: {
author: true, // Single query with join
},
});
```
### Use pagination
```typescript
async function getPaginatedUsers(page: number, pageSize: number = 10) {
return db.select()
.from(users)
.limit(pageSize)
.offset((page - 1) * pageSize);
}
```
### Batch inserts
**Bad:**
```typescript
for (const user of users) {
await db.insert(users).values(user); // N queries
}
```
**Good:**
```typescript
await db.insert(users).values(users); // Single query
```
## Type Safety
### Infer types from schema
```typescript
type User = typeof users.$inferSelect;
type NewUser = typeof users.$inferInsert;
const user: User = {
id: 1,
email: 'user@example.com',
name: 'John',
createdAt: new Date(),
};
const newUser: NewUser = {
email: 'user@example.com',
name: 'John',
};
```
### Type-safe WHERE conditions
```typescript
function getUsersByStatus(status: User['status']) {
return db.select()
.from(users)
.where(eq(users.status, status));
}
```
### Type-safe updates
```typescript
function updateUser(id: number, data: Partial<NewUser>) {
return db.update(users)
.set(data)
.where(eq(users.id, id))
.returning();
}
```
## Common Patterns
### Soft deletes
**Schema:**
```typescript
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
title: text('title').notNull(),
deletedAt: timestamp('deleted_at'),
});
```
**Queries:**
```typescript
const activePosts = await db.select()
.from(posts)
.where(isNull(posts.deletedAt));
const deletedPosts = await db.select()
.from(posts)
.where(isNotNull(posts.deletedAt));
```
### Timestamps
**Auto-update:**
```typescript
async function updatePost(id: number, data: Partial<NewPost>) {
return db.update(posts)
.set({
...data,
updatedAt: new Date(),
})
.where(eq(posts.id, id))
.returning();
}
```
### Search
**Simple search:**
```typescript
const searchUsers = await db.select()
.from(users)
.where(
or(
ilike(users.name, `%${query}%`),
ilike(users.email, `%${query}%`)
)
);
```
**Full-text search:**
```typescript
const searchPosts = await db.select()
.from(posts)
.where(
sql`to_tsvector('english', ${posts.title} || ' ' || ${posts.content}) @@ plainto_tsquery('english', ${query})`
);
```
### Unique constraints
**Handle duplicates:**
```typescript
try {
await db.insert(users).values({ email: 'user@example.com' });
} catch (err) {
if (err.code === '23505') { // Unique violation
console.error('Email already exists');
}
}
```
**Or use upsert:**
```typescript
await db.insert(users)
.values({ email: 'user@example.com', name: 'John' })
.onConflictDoUpdate({
target: users.email,
set: { name: 'John Updated' },
});
```
## Related Resources
- `guides/schema-only.md` - Schema design patterns
- `references/adapters.md` - Transaction availability by adapter
- `guides/troubleshooting.md` - Query error solutions
- `templates/schema-example.ts` - Complete schema with relations