ساختن بک‌اند تایپ‌اسکریپتی - بخش ۳: دیتابیس!

NestJS
NestJS

سرفصل‌ها

  1. فریمورک NestJS چیست و چرا؟
  2. شروع پروژه Todo
  3. دیتابیس!
  4. هر کاربر، Todoهای متفاوت

اگر این سری نوشته‌ها رو دنبال کرده باشید، میدونید که رسیدیم به اونجا که API خودمون رو برای اپلیکیشن ذخیره Todoها ایجاد کردیم و عملیات ایجاد کردن Todo، لیست کردن و گرفتن جزئیات یک Todo، آپدیت و حذف رو پیاده‌سازی کردیم و اطلاعات رو ذخیره می‌کردیم، ولی توی مموری!

برای کار با دیتابیس و ذخیره‌سازی دائمی داده‌ها، NestJS ماژولی برای کار با TypeORM (که بهترین ORM تایپ‌اسکریپتی هست به نظر من :)) ارائه داده که کار رو ساده خیلی می‌کنه.

در ادامه این نوشته، با استفاده از این ORM اپلیکیشنمون رو تغییر میدیم و داده‌ها رو در دیتابیس ذخیره می‌کنیم. بریم بیایم.


قبل از شروع

توی بخش قبلی ما ماژول Todo رو ساختیم و همون رو به عنوان پارامتر به متد create از NestFactory دادیم. ولی با گسترش اپلیکیشن ما فقط ۱ ماژول نداریم که بتونیم این کار رو بکنیم، برای همین ماژولی رو در نظر میگیریم که وظیفه اون فقط و فقط import کردن بقیه ماژول‌ها هست (هیچ سرویس و کنترلی نداره) و اون رو میدیم به NestFactory. من اسم این ماژول رو میذارم BootstrapModule، به این صورت:

bootstrap.module.ts
bootstrap.module.ts

حالا دیگه میتونیم بقیه ماژول‌هامون رو هم پیاده‌سازی کنیم و خیلی راحت اینجا import کنیم تا لود بشه.

TypeORM

این ORM بر روی ایده‌هایی از Hibernate توی جاوا و Entity Framework توی .NET بنا شده و با استفاده از Decoratorها کار با دیتابیس و عملیات‌های مختلف روی اون رو تا حد زیادی ساده کرده.

برای شروع باید این رو پکیج رو نصب کنیم:

npm install typeorm @nestjs/typeorm

برای این آموزش ما از دیتابیس sqlite استفاده می‌کنیم که باید درایور اون رو هم نصب کنیم

npm install sqilte3

بعد از اون، ماژول TypeORM رو توی اپلیکیشن خودمون import می‌کنیم و کانفیگ دیتابیس رو بهش میدیم تا بتونه وصل بشه. کجا بهتر از BootstrapModule میتونه باشه P:

ایمپورت TypeORM
ایمپورت TypeORM

توی چند خط اضافه شده، همونطور که قبلا ذکر شد، ماژول TypeOrm رو import کردیم و کانفیگ دیتابیس رو بهش پاس دادیم. متد forRoot یکبار برای اتصال اپلیکیشن به دیتابیس استفاده میشه و متد دیگری به نام forFeature هم وجود داره که جلوتر با اون آشنا میشیم.

خط اول توی آبجکت کانفیگ، نوع دیتابیس هست. اینجا از دیتابیس sqlite استفاده می‌کنیم که کل دیتابیس رو توی یک فایل ذخیره می‌کنه و ساده و جمع و جور هست. برای دیتابیس‌های دیگه‌ای که توی production استفاده میشه کانفیگ متفاوتی نیاز هست که تمامی اون‌هارو می‌تونید توی این لینک ببینید.

خط دوم کانفیگ، آدرس فایل دیتابیس هست. اگر قبلا دیتابیس خاصی نداشتید هر اسمی میتونید بدید و در صورت نبود اون، اپلیکیشن فایل رو درست می‌کنه.

خط سوم و چهارم توی همه نوع دیتابیس‌ها مشترک هست. خط سوم آدرس موجودیت‌های سیستم رو تعریف می‌کنه (TypeORM برای خوندن decoratorها و ایجاد اطلاعات اون‌ها به این گزینه نیاز داره). خط آخر هم جداول دیتابیس رو با چیزایی که توی اپلیکیشن تعریف کردیم یا تغییر دادیم همگام‌سازی می‌کنه. این گزینه سرعت توسعه رو خیلی بالا میبره و برای محیط توسعه عالی هست. ولی برای production باید غیرفعال بشه.

شناسایی Entity?

توی بخش قبل ما Entity سیستم رو تعریف کردیم. برای اینکه TypeORM بتونه اون رو شناسایی کنه و به خوبی توی دیتابیس جدول رو بسازه و داده‌ها رو ذخیره کنه، باید از decoratorهای TypeORM استفاده کنیم و Entity خودمون رو برای TypeORM تعریف کنیم.

todo.entity.ts
todo.entity.ts

با یه نگاه ساده میشه فهمید که هر کدوم از Decoratorها چه کاری می‌کنه.

دکوراتور Entity که مشخص می‌کنه این کلاسمون یک Entity درون سیستم هست و باید برای اون یک Table توی دیتابیس درست بشه.

برای مشخص کردن ستون‌های دیتابیس هم دکوراتورهای مختلفی وجود داره که چندتاش رو استفاده کردیم.

برای ستون‌های عادی از دکوراتور Column استفاده میشه. در حالت عادی نوع ستون با توجه به نوع فیلد به صورت اتوماتیک انتخاب میشه اما برای انواع پیچیده‌تر، باید نوع ستون رو به عنوان ورودی به این دکوراتور پاس بدید. برای کنترل بیشتر (مثل طول ستون کاراکتری) هم این دکوراتور آبجکتی دریافت می‌کنه که این تنظیمات رو مشخص می‌کنه. مثلا nullable بودن فیلد مثل completeTime. توی این قسمت داکیومنت TypeORM توضیحات کامل این موارد قرار داده شده.

دکوراتور PrimaryGeneratedColumn برای مشخص کردن ستون به عنوان کلید اصلی و generated (که به صورت اتوماتیک توسط دیتابیس تولید میشه) به صورت همزمان هست.

فیلدهای حاوی دکوراتورهای CreateDateColumn و UpdateDateColumn هم به صورت اتوماتیک هنگام ایجاد و آپدیت این entity، ایجاد و آپدیت میشن.

حالا برای اینکه توی ماژول خودمون، یعنی TodoModule به TypeORM و ریپاسیتوری‌های این entity دسترسی داشته باشیم، باید ماژول TypeORM رو با متد forFeature ایمپورت کنیم و entity های مورد نظرمون رو به عنوان پارامتر به این متد پاس بدیم!

todo.module.ts
todo.module.ts

با افزودن این چند خط، به ماژول TypeORM توی این ماژول دسترسی خواهیم داشت. همچنین repository ایجاد شده برای entityمون Todo رو میتونیم داشته باشیم.


مرحله آخر

خب الان همه چیز سر جاشه? باید از قابلیت‌هایی که تا اینجا بدست آوردیم، استفاده کنیم و داده‌هامون رو ذخیره کنیم. این دفعه توی دیتابیس و برای همیشه.

جایی از برنامه که باید تغییر کنه، سرویس‌هامون هست. سرویس Todo رو به شکل زیر تغییر میدیم.

todo.service.ts
todo.service.ts

خب! تغییراتی که inject کردن Repository توی constructor و استفاده از این Repository توی همه متدهامون هست. (storage و lastId که برای ذخیره توی مموری نیاز داشتیم رو هم حذف کردیم).

در حالت عادی برای استفاده از Injection اینکه تایپ فیلد رو داشته باشیم کافی هست، اما چون Repository تایپ Generic داره و قابلیت‌های Typescript برای خوندن تایپ‌ها توی برنامه کم هست، به دکوراتور InjectRepository نیاز داریم.

اول این نکته رو بگم که Repository پیاده‌سازی شده طبق Repository Pattern هست. طبق این پترن شما مستقیماً با دیتابیس تعامل ندارید و از کلاس‌های repository برای ذخیره و حذف و ... استفاده میشه. اینطوری راحت‌تر میشه تست کرد و تغییر داد قسمت‌های مختلف رو.

حالا توی متدهای مختلفمون از این repository استفاده می‌کنیم و کارهایی که قبلا توی مموری انجام میدادیم رو با دیتابیس انجام میدیم.

چون کار با دیتابیس زمان‌بر هست و async انجام میشه، همه متدهامون async هستن و خروجی اون‌ها Promise خواهد بود.

توی متد getTodo از Repository.findOneOrFail استفاده کردیم که یک آبجکت با شرایط خاص (یا مثل اینجا id خاص) رو برمیگردونه. اگر همچین آبجکتی وجود نداشت، exceptionی رخ میده که اینجا هندل نکردیم و کاربر اون رو میبینه.

توی متد getTodos هم از Repository.find استفاده کردیم که آرایه‌ای از آبجکت‌ها با شرایط خاص رو برمیگردونه.

توی متد createTodo، آبجکت Todo رو داشتیم و توسط متد Repository.save اون رو توی دیتابیس ذخیره کردیم. توجه داشته باشید که خروجی این متد آبجکت کاملمون هست که توی دیتابیس ذخیره شده. برای مثال توی ورودی ما createTime و updateTime وجود نداشت اما توی آیجکتی که از Repository.save میگیریم این فیلدها نیز مقداردهی شده.

توی متد updateTodo، آبجکت با آیدی که میخوایم آپدیت کنیم رو از دیتابیس گرفتیم. با استفاده از اون و مقادیر جدیدی که کاربر بهمون داده، آبجکت جدیدی درست کردیم و اون آبجکت جدید رو توی دیتابیس ذخیره کردیم. به دلیل اینکه id یا همون Primary Column مون توی آبجکت نهایی وجود داره، عملیات update با استفاده از Repository.save رخ میده. اما توی متد قبلی همین save رو برای create استفاده کردیم.

توی متد deleteTodo هم از Repository.delete استفاده کردیم که آبجکت‌های با شرایط خاص (یا مثل متدهای قبلی با id خاص) رو حذف می‌کنه.

خب سرویسمون با این تغییرات، از این به بعد todo ها رو توی دیتابیس ذخیره می‌کنه. و نیازی به دست زدن به بقیه اجزای برنامه نداریم. تنها تغییر کنترلرها، افزودن async/await هست که به خوانایی برنامه کمک می‌کنه.

todo.controller.ts
todo.controller.ts

همین دیگه.

حالا با اجرای برنامه، فایل دیتابیس sqlite ساخته میشه. به دلیل اینکه توی تنظیمات TypeORM گزینه synchronize رو برابر true قرار دادیم، دیتابیس با Entityهای ما همگام‌سازی میشه و اگر با برنامه‌ای که دیتابیس sqlite رو میخونه، فایل دیتابیس رو باز کنید میبینید که جداول مطابق اون چیزی که خواسته بودیم ساخته شدن.

مثل بخش قبل میتونیم آدرس‌های مختلف رو تست کنیم؛ این قسمت رو به خودتون میسپارم P-:


جمع بندی

توی این بخش، اطلاعات برنامه رو توی دیتابیس نگهداری کردیم تا با بستن و اجرا کردن دوباره برنامه اطلاعاتمون از بین نره. در این راه از sqlite به عنوان دیتابیس و TypeORM به عنوان لایه ارتباط entity ها با دیتابیس استفاده کردیم.

مشکل بعدی

مشکلی که هنوز حل نکردیم این هست که همه اطلاعات برای همه کاربران قابل دسترسی هست. ولی خب Todoهای هر کسی با Todoهای کس دیگه فرق داره. بخش بعدی به این میپردازیم که چگونه کاربر توی سیستم تعریف کنیم، اون رو احراز هویت کنیم درون سیستم و Todoهای مخصوص خودش رو براش بسازیم!


به پیشنهاد Majid ganji سورس پروژه رو روی گیت‌هات قرار دادم. از اینجا میتونید ببینید.