Initial commit
This commit is contained in:
779
skills/astro-db-patterns/SKILL.md
Normal file
779
skills/astro-db-patterns/SKILL.md
Normal file
@@ -0,0 +1,779 @@
|
||||
# Astro DB Patterns Skill
|
||||
|
||||
Common patterns and examples for using Astro DB, including schema design, queries, and data seeding.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- Setting up Astro DB in a project
|
||||
- Designing database schemas
|
||||
- Writing queries with Drizzle ORM
|
||||
- Seeding development data
|
||||
- Building API endpoints with database access
|
||||
|
||||
## Database Setup
|
||||
|
||||
### Install Astro DB
|
||||
|
||||
```bash
|
||||
npx astro add db
|
||||
```
|
||||
|
||||
### Configuration File
|
||||
|
||||
Create `db/config.ts`:
|
||||
|
||||
```typescript
|
||||
import { defineDb, defineTable, column } from 'astro:db';
|
||||
|
||||
const MyTable = defineTable({
|
||||
columns: {
|
||||
id: column.number({ primaryKey: true }),
|
||||
// ... other columns
|
||||
}
|
||||
});
|
||||
|
||||
export default defineDb({
|
||||
tables: { MyTable }
|
||||
});
|
||||
```
|
||||
|
||||
## Column Types
|
||||
|
||||
### Text Columns
|
||||
|
||||
```typescript
|
||||
// Basic text
|
||||
name: column.text()
|
||||
|
||||
// Unique text
|
||||
email: column.text({ unique: true })
|
||||
|
||||
// Optional text
|
||||
bio: column.text({ optional: true })
|
||||
|
||||
// Text with default
|
||||
status: column.text({ default: 'active' })
|
||||
```
|
||||
|
||||
### Number Columns
|
||||
|
||||
```typescript
|
||||
// Basic number
|
||||
age: column.number()
|
||||
|
||||
// Primary key
|
||||
id: column.number({ primaryKey: true })
|
||||
|
||||
// Optional number
|
||||
rating: column.number({ optional: true })
|
||||
|
||||
// Number with default
|
||||
views: column.number({ default: 0 })
|
||||
|
||||
// Unique number
|
||||
userId: column.number({ unique: true })
|
||||
```
|
||||
|
||||
### Boolean Columns
|
||||
|
||||
```typescript
|
||||
// Basic boolean
|
||||
published: column.boolean()
|
||||
|
||||
// Boolean with default
|
||||
active: column.boolean({ default: true })
|
||||
featured: column.boolean({ default: false })
|
||||
|
||||
// Optional boolean
|
||||
verified: column.boolean({ optional: true })
|
||||
```
|
||||
|
||||
### Date Columns
|
||||
|
||||
```typescript
|
||||
// Basic date
|
||||
createdAt: column.date()
|
||||
|
||||
// Optional date
|
||||
publishedAt: column.date({ optional: true })
|
||||
|
||||
// Date with default (requires runtime value)
|
||||
updatedAt: column.date({ default: new Date() })
|
||||
```
|
||||
|
||||
### JSON Columns
|
||||
|
||||
```typescript
|
||||
// JSON data
|
||||
metadata: column.json()
|
||||
|
||||
// JSON with default
|
||||
settings: column.json({ default: {} })
|
||||
options: column.json({ default: [] })
|
||||
|
||||
// Optional JSON
|
||||
extra: column.json({ optional: true })
|
||||
```
|
||||
|
||||
## Common Schema Patterns
|
||||
|
||||
### Blog Database
|
||||
|
||||
```typescript
|
||||
import { defineDb, defineTable, column } from 'astro:db';
|
||||
|
||||
const Author = defineTable({
|
||||
columns: {
|
||||
id: column.number({ primaryKey: true }),
|
||||
name: column.text(),
|
||||
email: column.text({ unique: true }),
|
||||
bio: column.text({ optional: true }),
|
||||
avatar: column.text({ optional: true }),
|
||||
website: column.text({ optional: true }),
|
||||
createdAt: column.date()
|
||||
}
|
||||
});
|
||||
|
||||
const Post = defineTable({
|
||||
columns: {
|
||||
id: column.number({ primaryKey: true }),
|
||||
title: column.text(),
|
||||
slug: column.text({ unique: true }),
|
||||
content: column.text(),
|
||||
excerpt: column.text({ optional: true }),
|
||||
published: column.boolean({ default: false }),
|
||||
publishedAt: column.date({ optional: true }),
|
||||
updatedAt: column.date({ optional: true }),
|
||||
authorId: column.number(),
|
||||
views: column.number({ default: 0 }),
|
||||
featured: column.boolean({ default: false })
|
||||
}
|
||||
});
|
||||
|
||||
const Tag = defineTable({
|
||||
columns: {
|
||||
id: column.number({ primaryKey: true }),
|
||||
name: column.text({ unique: true }),
|
||||
slug: column.text({ unique: true })
|
||||
}
|
||||
});
|
||||
|
||||
const PostTag = defineTable({
|
||||
columns: {
|
||||
postId: column.number(),
|
||||
tagId: column.number()
|
||||
}
|
||||
});
|
||||
|
||||
const Comment = defineTable({
|
||||
columns: {
|
||||
id: column.number({ primaryKey: true }),
|
||||
postId: column.number(),
|
||||
author: column.text(),
|
||||
email: column.text(),
|
||||
content: column.text(),
|
||||
approved: column.boolean({ default: false }),
|
||||
createdAt: column.date()
|
||||
}
|
||||
});
|
||||
|
||||
export default defineDb({
|
||||
tables: { Author, Post, Tag, PostTag, Comment }
|
||||
});
|
||||
```
|
||||
|
||||
### E-commerce Database
|
||||
|
||||
```typescript
|
||||
const Product = defineTable({
|
||||
columns: {
|
||||
id: column.number({ primaryKey: true }),
|
||||
name: column.text(),
|
||||
slug: column.text({ unique: true }),
|
||||
description: column.text(),
|
||||
price: column.number(),
|
||||
salePrice: column.number({ optional: true }),
|
||||
sku: column.text({ unique: true }),
|
||||
inStock: column.boolean({ default: true }),
|
||||
quantity: column.number({ default: 0 }),
|
||||
categoryId: column.number(),
|
||||
images: column.json({ default: [] }),
|
||||
createdAt: column.date()
|
||||
}
|
||||
});
|
||||
|
||||
const Category = defineTable({
|
||||
columns: {
|
||||
id: column.number({ primaryKey: true }),
|
||||
name: column.text(),
|
||||
slug: column.text({ unique: true }),
|
||||
description: column.text({ optional: true }),
|
||||
parentId: column.number({ optional: true })
|
||||
}
|
||||
});
|
||||
|
||||
const Order = defineTable({
|
||||
columns: {
|
||||
id: column.number({ primaryKey: true }),
|
||||
orderNumber: column.text({ unique: true }),
|
||||
customerId: column.number(),
|
||||
total: column.number(),
|
||||
status: column.text({ default: 'pending' }),
|
||||
createdAt: column.date(),
|
||||
completedAt: column.date({ optional: true })
|
||||
}
|
||||
});
|
||||
|
||||
const OrderItem = defineTable({
|
||||
columns: {
|
||||
id: column.number({ primaryKey: true }),
|
||||
orderId: column.number(),
|
||||
productId: column.number(),
|
||||
quantity: column.number(),
|
||||
price: column.number()
|
||||
}
|
||||
});
|
||||
|
||||
export default defineDb({
|
||||
tables: { Product, Category, Order, OrderItem }
|
||||
});
|
||||
```
|
||||
|
||||
### User Management Database
|
||||
|
||||
```typescript
|
||||
const User = defineTable({
|
||||
columns: {
|
||||
id: column.number({ primaryKey: true }),
|
||||
username: column.text({ unique: true }),
|
||||
email: column.text({ unique: true }),
|
||||
passwordHash: column.text(),
|
||||
firstName: column.text(),
|
||||
lastName: column.text(),
|
||||
role: column.text({ default: 'user' }),
|
||||
active: column.boolean({ default: true }),
|
||||
emailVerified: column.boolean({ default: false }),
|
||||
lastLogin: column.date({ optional: true }),
|
||||
createdAt: column.date()
|
||||
}
|
||||
});
|
||||
|
||||
const Session = defineTable({
|
||||
columns: {
|
||||
id: column.text({ primaryKey: true }),
|
||||
userId: column.number(),
|
||||
expiresAt: column.date(),
|
||||
createdAt: column.date()
|
||||
}
|
||||
});
|
||||
|
||||
const UserProfile = defineTable({
|
||||
columns: {
|
||||
userId: column.number({ unique: true }),
|
||||
bio: column.text({ optional: true }),
|
||||
avatar: column.text({ optional: true }),
|
||||
location: column.text({ optional: true }),
|
||||
website: column.text({ optional: true }),
|
||||
social: column.json({ default: {} })
|
||||
}
|
||||
});
|
||||
|
||||
export default defineDb({
|
||||
tables: { User, Session, UserProfile }
|
||||
});
|
||||
```
|
||||
|
||||
## Data Seeding
|
||||
|
||||
### Basic Seed File
|
||||
|
||||
Create `db/seed.ts`:
|
||||
|
||||
```typescript
|
||||
import { db, Author, Post, Tag } from 'astro:db';
|
||||
|
||||
export default async function seed() {
|
||||
// Insert authors
|
||||
await db.insert(Author).values([
|
||||
{
|
||||
id: 1,
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
bio: 'Tech enthusiast and blogger',
|
||||
createdAt: new Date('2024-01-01')
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Jane Smith',
|
||||
email: 'jane@example.com',
|
||||
bio: 'Software developer',
|
||||
createdAt: new Date('2024-01-01')
|
||||
}
|
||||
]);
|
||||
|
||||
// Insert tags
|
||||
await db.insert(Tag).values([
|
||||
{ id: 1, name: 'Astro', slug: 'astro' },
|
||||
{ id: 2, name: 'JavaScript', slug: 'javascript' },
|
||||
{ id: 3, name: 'Tutorial', slug: 'tutorial' }
|
||||
]);
|
||||
|
||||
// Insert posts
|
||||
await db.insert(Post).values([
|
||||
{
|
||||
id: 1,
|
||||
title: 'Getting Started with Astro',
|
||||
slug: 'getting-started-astro',
|
||||
content: 'Full post content here...',
|
||||
excerpt: 'Learn the basics of Astro',
|
||||
published: true,
|
||||
publishedAt: new Date('2024-01-15'),
|
||||
authorId: 1,
|
||||
views: 150,
|
||||
featured: true
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Advanced Astro Patterns',
|
||||
slug: 'advanced-astro',
|
||||
content: 'Advanced content...',
|
||||
published: false,
|
||||
authorId: 2,
|
||||
views: 0
|
||||
}
|
||||
]);
|
||||
}
|
||||
```
|
||||
|
||||
### Seed with Relationships
|
||||
|
||||
```typescript
|
||||
import { db, Post, Tag, PostTag, Comment } from 'astro:db';
|
||||
|
||||
export default async function seed() {
|
||||
// ... insert posts and tags ...
|
||||
|
||||
// Link posts to tags (many-to-many)
|
||||
await db.insert(PostTag).values([
|
||||
{ postId: 1, tagId: 1 }, // Post 1 -> Astro
|
||||
{ postId: 1, tagId: 3 }, // Post 1 -> Tutorial
|
||||
{ postId: 2, tagId: 1 }, // Post 2 -> Astro
|
||||
{ postId: 2, tagId: 2 } // Post 2 -> JavaScript
|
||||
]);
|
||||
|
||||
// Add comments
|
||||
await db.insert(Comment).values([
|
||||
{
|
||||
id: 1,
|
||||
postId: 1,
|
||||
author: 'Reader',
|
||||
email: 'reader@example.com',
|
||||
content: 'Great post!',
|
||||
approved: true,
|
||||
createdAt: new Date()
|
||||
}
|
||||
]);
|
||||
}
|
||||
```
|
||||
|
||||
## Query Patterns
|
||||
|
||||
### Select All
|
||||
|
||||
```typescript
|
||||
import { db, Post } from 'astro:db';
|
||||
|
||||
const allPosts = await db.select().from(Post);
|
||||
```
|
||||
|
||||
### Select with Filter
|
||||
|
||||
```typescript
|
||||
import { db, Post, eq } from 'astro:db';
|
||||
|
||||
// Published posts
|
||||
const publishedPosts = await db
|
||||
.select()
|
||||
.from(Post)
|
||||
.where(eq(Post.published, true));
|
||||
|
||||
// Post by slug
|
||||
const post = await db
|
||||
.select()
|
||||
.from(Post)
|
||||
.where(eq(Post.slug, 'my-post'))
|
||||
.get(); // Returns single result or undefined
|
||||
```
|
||||
|
||||
### Multiple Conditions
|
||||
|
||||
```typescript
|
||||
import { db, Post, eq, gt, and, or } from 'astro:db';
|
||||
|
||||
// AND condition
|
||||
const featuredPublished = await db
|
||||
.select()
|
||||
.from(Post)
|
||||
.where(and(
|
||||
eq(Post.published, true),
|
||||
eq(Post.featured, true)
|
||||
));
|
||||
|
||||
// OR condition
|
||||
const popularOrFeatured = await db
|
||||
.select()
|
||||
.from(Post)
|
||||
.where(or(
|
||||
gt(Post.views, 1000),
|
||||
eq(Post.featured, true)
|
||||
));
|
||||
```
|
||||
|
||||
### Comparison Operators
|
||||
|
||||
```typescript
|
||||
import { db, Post, gt, gte, lt, lte, ne, like } from 'astro:db';
|
||||
|
||||
// Greater than
|
||||
const popularPosts = await db
|
||||
.select()
|
||||
.from(Post)
|
||||
.where(gt(Post.views, 100));
|
||||
|
||||
// Greater than or equal
|
||||
const recentPosts = await db
|
||||
.select()
|
||||
.from(Post)
|
||||
.where(gte(Post.publishedAt, new Date('2024-01-01')));
|
||||
|
||||
// Less than
|
||||
const drafts = await db
|
||||
.select()
|
||||
.from(Post)
|
||||
.where(lt(Post.views, 10));
|
||||
|
||||
// Not equal
|
||||
const notDrafts = await db
|
||||
.select()
|
||||
.from(Post)
|
||||
.where(ne(Post.published, false));
|
||||
|
||||
// LIKE pattern matching
|
||||
const astroPosts = await db
|
||||
.select()
|
||||
.from(Post)
|
||||
.where(like(Post.title, '%Astro%'));
|
||||
```
|
||||
|
||||
### Ordering Results
|
||||
|
||||
```typescript
|
||||
import { db, Post, desc, asc } from 'astro:db';
|
||||
|
||||
// Descending order
|
||||
const latestPosts = await db
|
||||
.select()
|
||||
.from(Post)
|
||||
.orderBy(desc(Post.publishedAt));
|
||||
|
||||
// Ascending order
|
||||
const oldestFirst = await db
|
||||
.select()
|
||||
.from(Post)
|
||||
.orderBy(asc(Post.createdAt));
|
||||
|
||||
// Multiple order columns
|
||||
const sorted = await db
|
||||
.select()
|
||||
.from(Post)
|
||||
.orderBy(desc(Post.featured), desc(Post.publishedAt));
|
||||
```
|
||||
|
||||
### Limit and Offset
|
||||
|
||||
```typescript
|
||||
import { db, Post, desc } from 'astro:db';
|
||||
|
||||
// Latest 10 posts
|
||||
const latest = await db
|
||||
.select()
|
||||
.from(Post)
|
||||
.orderBy(desc(Post.publishedAt))
|
||||
.limit(10);
|
||||
|
||||
// Pagination
|
||||
const page = 2;
|
||||
const perPage = 10;
|
||||
const paginated = await db
|
||||
.select()
|
||||
.from(Post)
|
||||
.limit(perPage)
|
||||
.offset((page - 1) * perPage);
|
||||
```
|
||||
|
||||
### Joins
|
||||
|
||||
```typescript
|
||||
import { db, Post, Author, eq } from 'astro:db';
|
||||
|
||||
// Inner join
|
||||
const postsWithAuthors = await db
|
||||
.select()
|
||||
.from(Post)
|
||||
.innerJoin(Author, eq(Post.authorId, Author.id));
|
||||
|
||||
// Access joined data
|
||||
postsWithAuthors.forEach(row => {
|
||||
console.log(row.Post.title);
|
||||
console.log(row.Author.name);
|
||||
});
|
||||
```
|
||||
|
||||
### Complex Join Query
|
||||
|
||||
```typescript
|
||||
import { db, Post, Author, Tag, PostTag, eq } from 'astro:db';
|
||||
|
||||
// Posts with authors and tags
|
||||
const postsWithDetails = await db
|
||||
.select({
|
||||
post: Post,
|
||||
author: Author,
|
||||
tag: Tag
|
||||
})
|
||||
.from(Post)
|
||||
.innerJoin(Author, eq(Post.authorId, Author.id))
|
||||
.innerJoin(PostTag, eq(Post.id, PostTag.postId))
|
||||
.innerJoin(Tag, eq(PostTag.tagId, Tag.id));
|
||||
```
|
||||
|
||||
## Insert, Update, Delete
|
||||
|
||||
### Insert Single Record
|
||||
|
||||
```typescript
|
||||
import { db, Post } from 'astro:db';
|
||||
|
||||
await db.insert(Post).values({
|
||||
title: 'New Post',
|
||||
slug: 'new-post',
|
||||
content: 'Content here',
|
||||
authorId: 1,
|
||||
published: false
|
||||
});
|
||||
```
|
||||
|
||||
### Insert Multiple Records
|
||||
|
||||
```typescript
|
||||
await db.insert(Tag).values([
|
||||
{ name: 'Tag 1', slug: 'tag-1' },
|
||||
{ name: 'Tag 2', slug: 'tag-2' },
|
||||
{ name: 'Tag 3', slug: 'tag-3' }
|
||||
]);
|
||||
```
|
||||
|
||||
### Update Records
|
||||
|
||||
```typescript
|
||||
import { db, Post, eq } from 'astro:db';
|
||||
|
||||
// Update single field
|
||||
await db
|
||||
.update(Post)
|
||||
.set({ views: 100 })
|
||||
.where(eq(Post.id, 1));
|
||||
|
||||
// Update multiple fields
|
||||
await db
|
||||
.update(Post)
|
||||
.set({
|
||||
published: true,
|
||||
publishedAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
})
|
||||
.where(eq(Post.slug, 'my-post'));
|
||||
```
|
||||
|
||||
### Delete Records
|
||||
|
||||
```typescript
|
||||
import { db, Post, Comment, eq, lt } from 'astro:db';
|
||||
|
||||
// Delete by ID
|
||||
await db
|
||||
.delete(Post)
|
||||
.where(eq(Post.id, 1));
|
||||
|
||||
// Delete with condition
|
||||
await db
|
||||
.delete(Comment)
|
||||
.where(lt(Comment.createdAt, new Date('2024-01-01')));
|
||||
```
|
||||
|
||||
## API Endpoint Patterns
|
||||
|
||||
### GET All Items
|
||||
|
||||
```typescript
|
||||
// src/pages/api/posts.json.ts
|
||||
import type { APIRoute } from 'astro';
|
||||
import { db, Post, desc } from 'astro:db';
|
||||
|
||||
export const GET: APIRoute = async () => {
|
||||
const posts = await db
|
||||
.select()
|
||||
.from(Post)
|
||||
.orderBy(desc(Post.publishedAt));
|
||||
|
||||
return new Response(JSON.stringify(posts), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
### GET Single Item
|
||||
|
||||
```typescript
|
||||
// src/pages/api/posts/[slug].json.ts
|
||||
import type { APIRoute } from 'astro';
|
||||
import { db, Post, eq } from 'astro:db';
|
||||
|
||||
export const GET: APIRoute = async ({ params }) => {
|
||||
const post = await db
|
||||
.select()
|
||||
.from(Post)
|
||||
.where(eq(Post.slug, params.slug))
|
||||
.get();
|
||||
|
||||
if (!post) {
|
||||
return new Response(JSON.stringify({ error: 'Not found' }), {
|
||||
status: 404,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify(post), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
### POST Create Item
|
||||
|
||||
```typescript
|
||||
// src/pages/api/posts.json.ts
|
||||
import type { APIRoute } from 'astro';
|
||||
import { db, Post } from 'astro:db';
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
try {
|
||||
const data = await request.json();
|
||||
|
||||
await db.insert(Post).values({
|
||||
title: data.title,
|
||||
slug: data.slug,
|
||||
content: data.content,
|
||||
authorId: data.authorId,
|
||||
published: false,
|
||||
createdAt: new Date()
|
||||
});
|
||||
|
||||
return new Response(JSON.stringify({ success: true }), {
|
||||
status: 201,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ error: error.message }), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### PUT/PATCH Update Item
|
||||
|
||||
```typescript
|
||||
// src/pages/api/posts/[id].json.ts
|
||||
import type { APIRoute } from 'astro';
|
||||
import { db, Post, eq } from 'astro:db';
|
||||
|
||||
export const PUT: APIRoute = async ({ params, request }) => {
|
||||
const data = await request.json();
|
||||
const id = parseInt(params.id);
|
||||
|
||||
await db
|
||||
.update(Post)
|
||||
.set({
|
||||
...data,
|
||||
updatedAt: new Date()
|
||||
})
|
||||
.where(eq(Post.id, id));
|
||||
|
||||
return new Response(JSON.stringify({ success: true }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
### DELETE Item
|
||||
|
||||
```typescript
|
||||
export const DELETE: APIRoute = async ({ params }) => {
|
||||
const id = parseInt(params.id);
|
||||
|
||||
await db.delete(Post).where(eq(Post.id, id));
|
||||
|
||||
return new Response(null, { status: 204 });
|
||||
};
|
||||
```
|
||||
|
||||
## Production Deployment
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```env
|
||||
# .env
|
||||
ASTRO_DB_REMOTE_URL=libsql://your-db.turso.io
|
||||
ASTRO_DB_APP_TOKEN=your-auth-token
|
||||
```
|
||||
|
||||
### Push Schema to Production
|
||||
|
||||
```bash
|
||||
astro db push --remote
|
||||
```
|
||||
|
||||
### Verify Remote Connection
|
||||
|
||||
```bash
|
||||
astro db verify --remote
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use Primary Keys**: Always define a primary key for each table
|
||||
2. **Unique Constraints**: Use `unique: true` for fields like email, slug
|
||||
3. **Default Values**: Provide sensible defaults for boolean/number fields
|
||||
4. **Optional Fields**: Mark truly optional fields with `optional: true`
|
||||
5. **Indexing**: Use unique constraints for fields you'll query often
|
||||
6. **Relationships**: Use foreign keys (number columns) to link tables
|
||||
7. **Dates**: Always use `column.date()` for timestamp fields
|
||||
8. **JSON**: Use JSON columns for flexible/nested data
|
||||
9. **Naming**: Use consistent naming (camelCase for columns, PascalCase for tables)
|
||||
10. **Seeding**: Keep seed data representative and minimal
|
||||
|
||||
## Tips
|
||||
|
||||
- Dev server auto-restarts on schema changes
|
||||
- Check `.astro/content.db` for local database
|
||||
- Use `.get()` for single results, omit for arrays
|
||||
- Drizzle ORM provides type-safe queries
|
||||
- Test queries in seed file first
|
||||
- Use transactions for related inserts
|
||||
- Consider indexes for frequently queried fields
|
||||
- Migrate carefully in production
|
||||
Reference in New Issue
Block a user