از پست قبلی که نوشتن برنامه Hello World با Vapor بود مدت زیادی گذشت. راستش قرار نبود اینقدر فاصله پستها زیاد باشن اما دلیلش این بود که، منتظر بودم تا ورژن ۴ فریمورک Vapor منتشر بشه و مطالب آموزشی رو با اون بنویسم، چون خیلی خیلی کاملتر و در یک قسمتهایی استفاده ازش آسانتر هم هست. ولی ظاهرا طبق گفته یکی از دستاندرکاران پروژه تا چند ماه دیگه خبری از انتشار ورژن ۴ نیست. پس فعلا با Vapor 3 در خدمتتون هستم :)
برای درک بهتر مطالب این سری نوشته ها بهتر است آشنایی کافی با زبان Swift بخصوص Generics و Closure و همچنین آشنایی اولیه با RESTful API داشته باشین.
از اونجایی که Vapor یک فریمورک Async و Non-blocking هست (برای آشنایی مختصر با Non-blocking و Async به این نوشته من مراجعه کنید) هیچ دستوری باعث توقف مراحل اجرای برنامه نمیشه و به سرعت از روی اون دستور عبور کرده و دستور بعدی اجرا میشه. اما اگر واقعا زمان اجرای تابعی طولانی بود (مثلا خواندن از روی دیتابیس) چه اتفاقی میافته؟ در این صورت چنین تابعی نمیتونه بلافاصله جواب نهایی رو که به فرض یک array از رکوردهای دیتابیس هست رو برگردونه، از طرفی برنامه هم صبر نمیکنه تا اطلاعات از طرف دیتابیس خوانده و فرستاده بشن. پس خروجی تابع در آینده یک array خواهد بود و فعلا یک قول (Promise) داده میشه که بعدا (پس از خوانده شدن موفق اطلاعات از دیتابیس) یک array تحویل داده بشه. به این نوع خروجی EventLoopFuture یا در Vapor به اختصار Future گفته میشه (در واقع Future یک type alias است که Vapor به جای EventLoopFuture استفاده میکند و هیچ فرقی ندارد).
اگر فرض کنیم تابع getPostsFromDatabase قرار باشه لیست پستهای وبلاگ رو به صورت [Post] برگردونه، بعد از صدا زدن این تابع مقدار متغیر posts از نوع EventLoopFuture خواهد بود نه خود [Post]
let posts = getPostsFromDatabase() // posts : EventLoopFuture<[Post]>
مقدار این متغییر در واقع یک array نیست و هیچ کدام از دستوراتی که روی array ها اجرا میشوند، بر روی این متغییر اجرا نخواهند شد. خوب حالا چطوری میتونیم این مغییر رو به array تبدیل کنیم و مثلا اولین المان اون رو بگیریم و در کنسول پرینتش کنیم یا روی تمامی اعضای اون array لوپ بزنیم؟
میشه Future رو مانند یک متغیر optional در نظر بگیریم که قبل از استفاده از اون باید حتما unwrap بشه. در حال حاضر تنها راه unwrap کردن یک EventLoopFuture استفاده از این دو دستور بالا است. (تا وقتی که امکان async/await به زبان Swift اضافه بشه، که احتمالا در ورژن 6 خواهد بود ?). به عنوان مثال در تابع قبلی به این صورت میتونیم توی همه Post ها لوپ بزنیم و title هر پست رو چاپ کنیم.
اگر داخل map قرار بود دوباره تابعی صدا زده بشه که باز هم جواب آن EventLoopFuture هست، مثلا میخواهیم برای هر post از دیتابیس کامنت های مربوط به اون پست خوانده بشه (این مثال صرفا برای نمایش استفاده از flatMap هست و به این صورت خواندن از دیتابیس کاملا اشتباه است) و به هر پست اضافه بشه، به جای map از flatMap استفاده میکنیم که بعدا با مثال توضیح داده میشه.
بله! مطلب در ابتدا تا حدودی به نظر پیچیده میاد. اما با چند بار استفاده کاملا به موضوع احاطه پیدا خواهید کرد و به راحتی از این دستورات استفاده میکنین. بذارین کمی از این دستورات در کدها استفاده کنیم تا بهتر متوجه بشیم.
برنامه Terminal رو باز کرده و دستور زیر را وارد کنید تا پروژه ساخته شده و در Xcode باز شود.
vapor new blog-api cd blog-api vapor xcode -y
برای اطمینان از صحت کارکرد پروژه ساخته شده، آن را کامپایل کنید. دکمههای (CMD + B) را بزنید تا با دیدن پیغام Build successful از این قضیه مطمئن بشیم.
فایل جدیدی به نام Post.swift به Xcode در فولدر Sources/App/Models اضافه کنید.
مطالب زیر را در فایل وارد کنید.
توضیحات قسمتهای مختلف کد:
۱- کتابخانه FluentSQLite برای ساختن مدل هایی هست که به دیتابیس SQLite متصل میشوند.
۲- از آنجایی که ORM فریمورک Vapor از دیتابیس های متعدد پشتیبانی میکند و در آن واحد در یک پروژه میشود با چند دیتابیس اتصال برقرار کرد، باید در زمان ساخت مدل مشخص کنیم که این مدل از چه نوع دیتابیسی استفاده میکند.
۳- فعلا مدل Post را به ساده ترین حالت تعریف میکنیم. کلاس Post سه پارامتر یا property داره که id یا شناسه یکتا در دیتابیس که از نوع Int است. title یا عنوان و body یا متن که هر دو از نوع String تعریف شده اند. دلیل اینکه id به صورت optional تعریف شده اینه که در زمانی که هنوز Post در دیتابیس ذخیره نشده است مقدار id وجود ندارد، پس باید به صورت optional تعریف بشه.
۴- به ساده ترین شکل ممکن یک متد initializer برای کلاس تعریف میکنیم.
۵- اضافه کردن امکانات از پیش آماده شده به مدل هایی که در Vapor ساخته میشن به صورت extension به کلاس مورد نظر اضافه میشن. وقتی پروتکل (protocol) از قبل تعریف شده Migration رو به کلاس Post اضافه کنیم در واقع اعلام میکنیم که کلاس Post از این پروتکل پیروی میکنه، و باعث میشه که در هنگام شروع برنامه، جدول مربوط به این مدل به طور خودکار در دیتابیس برنامه ساخته بشه (اگر قبلا ساخته نشده باشه). برای مدل های ساده مثل مدل فعلی ما لازم نیست کدی برای Migration بنویسیم و ساخت جدول به صورت خودکار کفایت میکنه. اما اگر مدل پیچیدهتر باشه میشه در این قسمت کدهای مربوطه رو نوشت.
۶- اگر با Codable در Swift آشنایی داشته باشین، میتونم بهتون بگم که Content در واقع همون Codable هست که چند تا پارامتر به خصوصیات اون اضافه شده. برای کسانی که با Codable آشنا نیستن بگم که کلاسی که از پروتکل Codable پیروی میکنه (conform) به راحتی قابل تبدیل شدن از مدل swift به JSON و بالعکس میباشد. وقتی داریم REST API مینویسیم معلومه که همیشه باید مدل هامون از پروتوکل Content پیروی کنن تا به راحتی تبدیل به JSON و ساخت مدل از روی JSON انجام بشه.
۷- وقتی مدل از پروتکل Parameter پیروی کنه، امکان تبدیل خودکار از طریق id مدل، که در url های API فرستاده میشه، به مدل swift رو پیدا میکنه . به این کار Route Model Binding گفته میشه. یعنی اگر چنین آدرسی رو داشته باشیم
http://my-blog.com/posts/99
هنگامی که این url فرا خونده میشه و متد مربوطه در برنامه ما صدا زده میشه، Vapor به طور خودکار اطلاعات پست با شناسه 99 رو از دیتابیس میخونه، تبدیل به مدل Post میکنه و در اختیار ما قرار میده بدون اینکه ما دستور اضافه ای رو صدا بزنیم.
معمولا پیروی مدلهای Vapor از این ۳ پروتکل برای عمده کارها کفایت میکنه و ما هم فعلا به همین بسنده میکنیم.
برای داشتن یک API کامل (RESTful) برای مدل Post حداقل به ۵ عدد route احتیاج داریم:
1- index برای نمایش لیست پست ها
Method: GET | url: /posts
2- show برای نمایش یک پست با شناسه مشخص
Method: GET | url: /posts/{id}
3- create برای ایجاد پست جدید
Method: POST | url: /posts
4- update برای بروزرسانی پست موجود با شناسه مشخص
Method: PATCH | url: /posts/{id}
5- delete برای حذف پست موجود با شناسه مشخص
Method: DELETE | url: /posts/{id}
فایل routes.swift رو باز کنید و مطالب موجود در اون رو با این مطالب جایگزین کنین.
توضیحات قسمتهای مختلف کد:
۱- یک متغیر از روی کلاس PostController ایجاد میکنیم. این کلاس هنوز وجود ندارد و در مرحله بعد اون رو میسازیم برای همین موقتا در Xcdoe خطا مشاهده خواهید کرد. این کلاس شامل تمام توابعی هست که برای نمایش، ساختن، بروزرسانی و پاک کردن پستها لازم است.
۲- روت های لازم که در بالا لیست اونها رو دیدیم تعریف میکنیم. دقت کنید Method هر روت مشخص است (get, post, patch, delete) . تمامی پارامتر های این توابع تا قبل از پارامتر use همگی تشکیل دهنده آدرس route هستند. Vapor این پارامترها را به هم متصل میکند تا آدرس URL مربوط به این تابع ساخته شود. مثلا برای اینکه تابعی داشته باشیم تا هنگام فراخوانی درخواست به آدرس زیر صدا زده بشه،
/posts/users/5
به این صورت نوشته میشه
router.get("posts", "users", User.parameter, use: UserController.showUserPosts)
مطلب مهم در این قسمت استفاده از parameter. برای Route Model Binding است. در کد مربوط به روت های Post چندین جا از Post.paramter استفاده شده. از اونجایی که کلاس Post ما از پروتکل Parameter پیروی میکنه میتونیم از این دستور در route ها استفاده کنیم. این دستور باعث میشه id پست مورد نظر که در URL وجود داره، از دیتابیس خوانده بشه و مدل دریافت شده از دیتابیس در اختیار ما گذاشته بشه.
پارامتر دوم تابع router، نام یک تابع از کلاس Controller است که هنگام فراخوانی URL مورد نظر ما اجرا میشود.
فایل جدیدی به اسم PostController.swift در دایرکتوری Sources/App/Controllers اضافه کنید و مطالب زیر را در آن وارد کنید.
توضیحات قسمتهای مختلف کد:
۱- تابع index برای گرفتن لیست پستهای موجود و دارای کد سادهای است. تنها کاری که انجام میدهد روی مدل Post یک کوئری میزند و لیست همه Postها را برمیگرداند. از آنجایی که کوئری زدن در Vapor یک عمل Async هست و نتیجه آن بعدا (زمانی که سرور دیتابیس جواب کوئری را برگرداند) مشخص میشود، ما در واقع یک array از پست ها - [Post] - رو برنمیگردونیم و داریم یک Future از نوع [Post] برمیگردونیم. که در این قسمت خود Vapor زحمت این کار رو میکشه و هنگامی که Future<[Post]> l به [Post] تبدیل شد (این قضیه بسته به پیچیدگی کوئری و بار سرور دیتابیس، ممکن است از چند هزارم ثانیه تا چندین ثانیه طول بکشد)، آن را تبدیل به JSON کرده (یادتون باشه که مدل Post از پروتکل Content پیروی میکرد) و نتیجه را به ما برمیگرداند.
مطلبی که ممکن است توجه شما را جلب کرده باشه اینه که چرا را متغیر req را به تابع query میفرستیم. به اختصار میشه اینطور گفت که چون عملیات Async در پشت صحنه بسیار پیچیده هستند و احتیاج به یک Worker دارند که متغیر req که از نوع Request میباشد در واقع یک نوع Container هست که درون خود یک یا چند Worker دارد. برای همین ما برای اجرای کوئری ها میتونیم با فرستادن req به تابع query نیاز آن به Worker را تامین کنیم.
۲- تابع show برای نمایش جزئیات یک پست استفاده میشه. id پست مورد نظر به عنوان پارامتر در URL فرستاده میشه و از انجایی که مدل Post از پروتکل Parameter پیروی میکنه Vapor میتونه id پست را از داخل URL تشخیص بده و پست مورد نظر رو از دیتابیس بگیره و به ما تحویل بده و کماکان چون این کار با کوئری زدن از دیتابیس انجام میشه جوابی که به دست ما میرسه یک Future از نوع Post هست که ما در انتهای تابع show آن را برمیگردانیم و بقیه کارها را Vapor انجام میدهد (تبدیل Future-Post به Post و پس از آن تبدیل Post به JSON)
۳- کار تابع create ذخیره کردن یک Post در دیتابیس است که مشخصات آن در واقع در بدنه ریکوئست (body) فرستاده شده اند. مثل title و body. در این مرحله با توجه به پارامترهای مربوط به پست جدید که در بدنه ریکوئست فرستاده شدهاند یک مدل Post ساخته میشه و اون رو در متغیر post ذخیره میکنیم.
۴- الان میتونیم تابع save را بر روی متغیر post صدا بزنیم و این مدل را در دیتابیس ذخیره کنیم و در انتها همان مدل ذخیره شده را برمیگردانیم.
۵- در تابع delete برای حذف یک پست از دیتابیس همانند تابع show باید id آن پست در URL فرستاده شود. از آنجایی که گرفتن آبجکت post مورد نظر از دیتابیس باعث میشود جواب داده شده Future باشد، و ما میخواهیم عمل delete را بر روی آن اعمال کنیم احتیاج به خود مدل Post داریم نه Future-Post، پس از flatMap استفاده میکنیم تا بتونیم به مدل Post دسترسی داشته باشیم
۶- تابع delete رو بر روی آبجکت post صدا میزنیم تا آن را از database حذف کند.
۷- معمولا جواب ریکوئست DELETE در صورت موفق بودن یک ریسپانز (HTTP Response) با بدنه خالی و با استتوس 204 است که به نام No-content مشهور مبیاشد. جواب تابع delete را با دستور transform به HTTPStatus.noContent تبدیل میکنیم و آن را برمیگردانیم.
۸- گرفتن پست موجود که id آن در URL آمده است و استفاده از flatMap برای دسترسی به مدل واقعی Post نه Future-Post
۹- آپدیت کردن یک مدل کمی متفاوت است. به ۲ دلیل:
برای رفع این موارد، ما یک مدل شبیه مدل Post میسازیم و پارامترهای اون رو همونطور که مد نظر خودمون هست تعریف میکنیم. برای اینکار یک struct جدید به نام UpdatablePost در کلاس Post تعریف میکنیم و هر دو پارامتر title و body آن رو به صورت optional معرفی میکنیم.
final class Post: SQLiteModel {
.... // existing code
struct UpdatablePost: Content {
var title: String?
var body: String?
}
}
حالا به جای اینکه بگیم پارامترهای موجود در بدنه ریکوئست رو به مدل Post تبدیل کنه، آنها رو به مدل Post.UpdatablePost تبدیل میکنیم و در متغییر newPostValues ذخیره میکنیم.
۱۰- در این قسمت چک میکنیم هر کدام از پارامترهایی که فرستاده شده اند چنانچه مقدار داشتند، مقدار آن را در آبجکت post بروزرسانی میکنیم. هر کدام هم مقداری نداشتند همان مقدار قبلی آن قرارداده میشود. (بدون تغییر میماند)
۱۱- تابع update را بر روی post صدا میزنیم تا هر کدام از مقادیر مشخصات آن تغییر کرده بودند در دیتابیس بروزرسانی و ذخیره شوند و در نهایت مدل بروزرسانی شده رو برمیگردانیم.
قبل از اینکه بتونیم کد مون رو کامپایل و اون رو تست کنیم چند مورد رو باید در فایل تنظیمات (config) تغییر بدیم.
فایل configure.swift رو باز کنید و مطالب موجود در اون رو با این مطالب جایگزین کنین.
۱- در این قسمت ما نوع ذخیره اطلاعات در دیتابیس sqlite رو در file انتخاب کرده و آدرس و نام فایل مورد نظرمون رو بهش میدیم. برای انجام تست میشه از حالت ذخیره در memory هم استفاده کرد که دیگه احتیاج به وارد کردن نام فایل نداره و پس از هر بار راه اندازی برنامه دیتابیس موجود در memory پاک میشه.
۲- در این قسمت به Vapor میگیم که مدل Post رو به لیست migration ها اضافه کن. هر مدلی که به این لیست اضافه بشه در هنگام راه اندازی برنامه چنانچه تیبل اون در دیتابیس ساخته نشده باشه، Vapor اون تیبل رو در دیتابیس ایجاد میکنه.
خوب! الان میتونین برنامه رو اجرا کنین. در Xcode از قسمت Scheme گزینه Run -> My Mac رو انتخاب کنید و دکمه Cmd+R رو بزنین و منتظر باشین تا کامپایل تموم بشه و در کنسول Xcode این عبارت رو ببینین.
[ INFO ] Migrating 'sqlite' database (.../blog/.build/checkouts/fluent/Sources/Fluent/Migration/MigrationConfig.swift:69) [ INFO ] Preparing migration 'Post' (.../blog/.build/checkouts/fluent/Sources/Fluent/Migration/Migrations.swift:111) [ INFO ] Migrations complete (.../blog/.build/checkouts/fluent/Sources/Fluent/Migration/MigrationConfig.swift:73) Running default command: ~/Library/Developer/Xcode/DerivedData/blog-btnxmupvzrtyhxgiuhlrbbibutsv/Build/Products/Debug/Run serve Server starting on http://localhost:8080
الان برنامه شما اجرا شده و روی پورت 8080 منتظر ارسال درخواست های شما هست، میتونین با برنامه REST Client دلخواه تون مثل Paw یا Postman تمامی قسمت های برنامه رو تست کنین. برای تستهای خیلی ساده و سریع من برنامه Rested رو پیشنهاد میکنم که به رایگان میتونین از Mac Appstore بگیرن. اما برای کارهای پیچیدهتر یکی از برنامههای Paw و Postman و Insomnia پیشنهاد میشه.
ضمنا کد این پروژه تا این مرحله در گیتهاب موجوده و میتونین اون رو دانلود کنین.
در قسمت های بعد به تعریف کاربر و اتصال پست ها به کاربران و نوشتن نظر برای هر پست میپردازیم.
نتیجه کارکرد قسمتهایی از برنامه رو در این عکسها میتونین ببینین