TypeScript مع قواعد البيانات
TypeScript مع قواعد البيانات
تجلب TypeScript أمان نوع قوي لتفاعلات قاعدة البيانات، مما يساعدك على اكتشاف عدم تطابق المخطط، والاستعلامات غير الصالحة، وعدم اتساق البيانات قبل وصولها إلى الإنتاج. في هذا الدرس، سنستكشف كيفية كتابة نماذج ORM، والعمل مع Prisma و TypeORM، وبناء أنماط استعلام قاعدة بيانات آمنة من حيث النوع توفر التحقق في وقت الترجمة وتجربة مطور ممتازة.
لماذا نكتب طبقة قاعدة البيانات؟
إضافة TypeScript إلى عمليات قاعدة البيانات الخاصة بك يوفر مزايا حاسمة:
- التحقق من المخطط: تأكد من أن الكود الخاص بك يطابق مخطط قاعدة البيانات الخاص بك في وقت الترجمة
- أمان الاستعلام: اكتشف الأخطاء المطبعية في أسماء الأعمدة وشروط التصفية غير الصالحة قبل وقت التشغيل
- الإكمال التلقائي: احصل على IntelliSense لجميع خصائص النموذج والعلاقات وطرق الاستعلام
- أمان الترحيل: أخطاء TypeScript تنبهك إلى الكود الذي يحتاج إلى تحديث عند تغيير المخطط
- سلامة البيانات: فرض الحقول المطلوبة وأنواع البيانات الصحيحة والعلاقات الصالحة
Prisma - ORM آمن من حيث النوع
Prisma هو ORM من الجيل التالي يولد أنواع TypeScript مباشرة من مخطط قاعدة البيانات الخاص بك. يوفر أفضل تكامل TypeScript لأي ORM.
// prisma/schema.prisma
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model User {
id Int @@id @@default(autoincrement())
email String @@unique
username String @@unique
firstName String
lastName String
avatar String?
posts Post[]
comments Comment[]
createdAt DateTime @@default(now())
updatedAt DateTime @@updatedAt
@@map("users")
}
model Post {
id Int @@id @@default(autoincrement())
title String
slug String @@unique
content String
excerpt String?
published Boolean @@default(false)
authorId Int
author User @@relation(fields: [authorId], references: [id])
comments Comment[]
tags Tag[]
viewCount Int @@default(0)
publishedAt DateTime?
createdAt DateTime @@default(now())
updatedAt DateTime @@updatedAt
@@map("posts")
}
model Comment {
id Int @@id @@default(autoincrement())
content String
postId Int
post Post @@relation(fields: [postId], references: [id])
userId Int
user User @@relation(fields: [userId], references: [id])
createdAt DateTime @@default(now())
@@map("comments")
}
model Tag {
id Int @@id @@default(autoincrement())
name String @@unique
posts Post[]
@@map("tags")
}
npx prisma generate لإنشاء كود عميل TypeScript مكتوب بالكامل. ينشئ Prisma تلقائيًا أنواعًا لجميع نماذجك، بما في ذلك العلاقات وعمليات الاستعلام.
استخدام عميل Prisma
يوفر عميل Prisma المولد أمان نوع كامل لجميع عمليات قاعدة البيانات:
import { PrismaClient, User, Post, Prisma } from '@prisma/client';
const prisma = new PrismaClient();
// إنشاء - إدخال مكتوب بالكامل
async function createUser(data: {
email: string;
username: string;
firstName: string;
lastName: string;
avatar?: string;
}): Promise<User> {
return prisma.user.create({
data,
});
}
// قراءة - قيمة إرجاع مكتوبة مع العلاقات
async function getUserWithPosts(id: number) {
return prisma.user.findUnique({
where: { id },
include: {
posts: {
where: { published: true },
orderBy: { createdAt: 'desc' },
},
},
});
}
// نوع الإرجاع يتم استنتاجه تلقائيًا:
// User & { posts: Post[] }
// تحديث - إدخال وإرجاع مكتوب
async function updateUser(
id: number,
data: Prisma.UserUpdateInput
): Promise<User> {
return prisma.user.update({
where: { id },
data,
});
}
// حذف
async function deleteUser(id: number): Promise<User> {
return prisma.user.delete({
where: { id },
});
}
استعلامات Prisma المتقدمة
يوفر Prisma قدرات استعلام متطورة مع أمان نوع كامل:
// استعلامات مفلترة مع علاقات متداخلة
async function searchPosts(search: string, limit: number = 10) {
return prisma.post.findMany({
where: {
OR: [
{ title: { contains: search, mode: 'insensitive' } },
{ content: { contains: search, mode: 'insensitive' } },
],
published: true,
},
include: {
author: {
select: {
id: true,
username: true,
avatar: true,
},
},
comments: {
where: { createdAt: { gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) } },
take: 3,
},
tags: true,
},
orderBy: {
publishedAt: 'desc',
},
take: limit,
});
}
// التجميع والتجميع
async function getPostStatistics() {
const stats = await prisma.post.aggregate({
_count: true,
_avg: { viewCount: true },
_sum: { viewCount: true },
_max: { viewCount: true },
where: { published: true },
});
return stats;
}
// استعلامات العد
async function getUserPostCount(userId: number) {
return prisma.post.count({
where: {
authorId: userId,
published: true,
},
});
}
// ترقيم الصفحات
async function getPaginatedPosts(page: number = 1, perPage: number = 20) {
const skip = (page - 1) * perPage;
const [posts, total] = await Promise.all([
prisma.post.findMany({
skip,
take: perPage,
where: { published: true },
include: { author: true, tags: true },
orderBy: { publishedAt: 'desc' },
}),
prisma.post.count({ where: { published: true } }),
]);
return {
data: posts,
meta: {
page,
perPage,
total,
totalPages: Math.ceil(total / perPage),
},
};
}
select و include في Prisma للتحكم بالضبط في الحقول والعلاقات التي يتم إرجاعها. ستضيق TypeScript تلقائيًا نوع الإرجاع بناءً على بنية الاستعلام الخاص بك.
معاملات Prisma
نفذ عمليات متعددة بشكل ذري مع معاملات آمنة من حيث النوع:
// معاملة تسلسلية
async function createPostWithTags(
authorId: number,
postData: {
title: string;
slug: string;
content: string;
excerpt?: string;
},
tagNames: string[]
) {
return prisma.$transaction(async (tx) => {
// إنشاء أو العثور على العلامات
const tags = await Promise.all(
tagNames.map((name) =>
tx.tag.upsert({
where: { name },
create: { name },
update: {},
})
)
);
// إنشاء منشور مع العلامات
const post = await tx.post.create({
data: {
...postData,
authorId,
tags: {
connect: tags.map((tag) => ({ id: tag.id })),
},
},
include: {
tags: true,
author: true,
},
});
return post;
});
}
// معاملة دفعية
async function deleteUserAndContent(userId: number) {
return prisma.$transaction([
prisma.comment.deleteMany({ where: { userId } }),
prisma.post.deleteMany({ where: { authorId: userId } }),
prisma.user.delete({ where: { id: userId } }),
]);
}
TypeORM - ORM قائم على المزخرفات
يستخدم TypeORM مزخرفات TypeScript لتعريف الكيانات ويوفر أمان نوع ممتاز:
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
ManyToOne,
OneToMany,
ManyToMany,
JoinTable,
Index,
} from 'typeorm';
@@Entity('users')
export class User {
@@PrimaryGeneratedColumn()
id: number;
@@Column({ unique: true })
@@Index()
email: string;
@@Column({ unique: true })
username: string;
@@Column()
firstName: string;
@@Column()
lastName: string;
@@Column({ nullable: true })
avatar?: string;
@@OneToMany(() => Post, (post) => post.author)
posts: Post[];
@@OneToMany(() => Comment, (comment) => comment.user)
comments: Comment[];
@@CreateDateColumn()
createdAt: Date;
@@UpdateDateColumn()
updatedAt: Date;
}
@@Entity('posts')
export class Post {
@@PrimaryGeneratedColumn()
id: number;
@@Column()
title: string;
@@Column({ unique: true })
slug: string;
@@Column('text')
content: string;
@@Column({ type: 'text', nullable: true })
excerpt?: string;
@@Column({ default: false })
published: boolean;
@@Column()
authorId: number;
@@ManyToOne(() => User, (user) => user.posts)
author: User;
@@OneToMany(() => Comment, (comment) => comment.post)
comments: Comment[];
@@ManyToMany(() => Tag, (tag) => tag.posts)
@@JoinTable()
tags: Tag[];
@@Column({ default: 0 })
viewCount: number;
@@Column({ nullable: true })
publishedAt?: Date;
@@CreateDateColumn()
createdAt: Date;
@@UpdateDateColumn()
updatedAt: Date;
}
@@Entity('comments')
export class Comment {
@@PrimaryGeneratedColumn()
id: number;
@@Column('text')
content: string;
@@Column()
postId: number;
@@ManyToOne(() => Post, (post) => post.comments)
post: Post;
@@Column()
userId: number;
@@ManyToOne(() => User, (user) => user.comments)
user: User;
@@CreateDateColumn()
createdAt: Date;
}
@@Entity('tags')
export class Tag {
@@PrimaryGeneratedColumn()
id: number;
@@Column({ unique: true })
name: string;
@@ManyToMany(() => Post, (post) => post.tags)
posts: Post[];
}
experimentalDecorators و emitDecoratorMetadata في tsconfig.json الخاص بك. بدون هذه الإعدادات، لن تعمل الكيانات القائمة على المزخرفات.
نمط مستودع TypeORM
يوفر TypeORM مستودعات مكتوبة لعمليات قاعدة البيانات:
import { AppDataSource } from './data-source';
import { User, Post } from './entities';
const userRepository = AppDataSource.getRepository(User);
const postRepository = AppDataSource.getRepository(Post);
// إنشاء
async function createUser(data: {
email: string;
username: string;
firstName: string;
lastName: string;
avatar?: string;
}): Promise<User> {
const user = userRepository.create(data);
return userRepository.save(user);
}
// قراءة مع العلاقات
async function getUserWithPosts(id: number): Promise<User | null> {
return userRepository.findOne({
where: { id },
relations: ['posts', 'posts.tags'],
});
}
// استعلام معقد مع QueryBuilder
async function searchPosts(search: string, limit: number = 10) {
return postRepository
.createQueryBuilder('post')
.leftJoinAndSelect('post.author', 'author')
.leftJoinAndSelect('post.tags', 'tags')
.where('post.published = :published', { published: true })
.andWhere(
'(post.title ILIKE :search OR post.content ILIKE :search)',
{ search: `%${search}%` }
)
.orderBy('post.publishedAt', 'DESC')
.take(limit)
.getMany();
}
// تحديث
async function updateUser(
id: number,
data: Partial<User>
): Promise<void> {
await userRepository.update(id, data);
}
// حذف
async function deleteUser(id: number): Promise<void> {
await userRepository.delete(id);
}
فئات المستودع المخصصة
أنشئ فئات مستودع مخصصة لمنطق المجال المعقد:
import { Repository } from 'typeorm';
import { Post } from './entities/Post';
export class PostRepository extends Repository<Post> {
async findPublished(limit: number = 10): Promise<Post[]> {
return this.find({
where: { published: true },
relations: ['author', 'tags'],
order: { publishedAt: 'DESC' },
take: limit,
});
}
async findBySlug(slug: string): Promise<Post | null> {
return this.findOne({
where: { slug },
relations: ['author', 'tags', 'comments', 'comments.user'],
});
}
async incrementViewCount(id: number): Promise<void> {
await this.increment({ id }, 'viewCount', 1);
}
async getMostViewed(limit: number = 5): Promise<Post[]> {
return this.find({
where: { published: true },
order: { viewCount: 'DESC' },
take: limit,
relations: ['author'],
});
}
async getPostsByTag(tagName: string, limit: number = 10): Promise<Post[]> {
return this.createQueryBuilder('post')
.leftJoinAndSelect('post.author', 'author')
.leftJoinAndSelect('post.tags', 'tags')
.where('tags.name = :tagName', { tagName })
.andWhere('post.published = :published', { published: true })
.orderBy('post.publishedAt', 'DESC')
.take(limit)
.getMany();
}
}
مساعدات نوع استعلام قاعدة البيانات
أنشئ مساعدات نوع قابلة لإعادة الاستخدام لأنماط الاستعلام الشائعة:
// نوع نتيجة مقسمة عام
interface PaginatedResult<T> {
data: T[];
meta: {
page: number;
perPage: number;
total: number;
totalPages: number;
};
}
// منشئ فلتر عام
type FilterOperator = 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'contains';
interface Filter<T> {
field: keyof T;
operator: FilterOperator;
value: any;
}
// خيارات الترتيب العامة
type SortDirection = 'asc' | 'desc';
interface SortOption<T> {
field: keyof T;
direction: SortDirection;
}
// مساعد منشئ الاستعلام
interface QueryOptions<T> {
filters?: Filter<T>[];
sort?: SortOption<T>[];
page?: number;
perPage?: number;
include?: string[];
}
// دالة مساعدة Prisma
async function queryWithOptions<T>(
model: any,
options: QueryOptions<T>
): Promise<PaginatedResult<T>> {
const { filters = [], sort = [], page = 1, perPage = 20, include = [] } = options;
const where: any = {};
filters.forEach((filter) => {
// بناء جملة where بناءً على المشغل
// التنفيذ يعتمد على ORM الخاص بك
});
const orderBy: any = {};
sort.forEach((s) => {
orderBy[s.field] = s.direction;
});
const skip = (page - 1) * perPage;
const [data, total] = await Promise.all([
model.findMany({
where,
orderBy,
skip,
take: perPage,
include: include.reduce((acc, rel) => ({ ...acc, [rel]: true }), {}),
}),
model.count({ where }),
]);
return {
data,
meta: {
page,
perPage,
total,
totalPages: Math.ceil(total / perPage),
},
};
}
- أنشئ مخطط Prisma مع 3 نماذج مرتبطة على الأقل (مثل Author، Book، Review)
- ولّد عميل Prisma واكتب دوال CRUD آمنة من حيث النوع لكل نموذج
- نفذ استعلامًا معقدًا يقوم بالفلترة والترتيب ويتضمن علاقات متداخلة
- أنشئ معاملة تحدث سجلات مرتبطة متعددة بشكل ذري
- قم ببناء فئة مستودع مخصصة مع طرق استعلام خاصة بالمجال
- نفذ مساعد ترقيم صفحات عام مع فلترة وترتيب آمنين من حيث النوع
ملخص
في هذا الدرس، تعلمت كيفية العمل مع قواعد البيانات في TypeScript باستخدام Prisma و TypeORM. استكشفت كيفية تعريف مخططات آمنة من حيث النوع، وإجراء عمليات CRUD مع فحص النوع الكامل، وبناء استعلامات معقدة مع دعم الإكمال التلقائي، وتنفيذ المعاملات وأنماط المستودع المخصصة. تمكّنك هذه التقنيات من اكتشاف الأخطاء المتعلقة بقاعدة البيانات في وقت الترجمة، والحفاظ على الاتساق بين الكود والمخطط الخاص بك، وبناء طبقات بيانات قوية مع تجربة مطور ممتازة.