خیلی از افرادی که این روزها، میخوان پروژههایی در حوزههای مختلف هوش مصنوعی مثل یادگیری ماشین، یادگیری عمیق، علوم داده و … انجام بدن یک گلوگاه بسیار بزرگ دارند و اون «داده» است. خیلیها واقعا نمیدونن از کجا میتونن دادههای مناسب پروژههاشون به دست بیارن. در این مطلب، قراره که این موضوع رو پوشش بدم.
منابع مناسب داده برای پروژههای شما
در این بخش، با هم چندین منبع مناسب برای پیدا کردن داده رو بررسی خواهیم کرد. فقط قبل از هرچیز این رو بگم که این منابع میتونن تغییر کنن در طول زمان پس هرچه که در این مطلب بیان شده رو در مرداد ۱۴۰۰ معتبر بدونید و اگر مدتی بعد از انتشار این مطلب دارید مطالعهش میکنید، با جستوجو و پرسوجو در مورد این منابع، اطلاعات بهروزتر دریافت کنید.
Kaggle
وبسایت کگل، یک محیط تقریبا مشابه شبکههای اجتماعی برای دانشمندان داده و متخصصین هوش مصنوعی به حساب میاد. در این وبسایت شما میتونید مجموعه داده (Dataset) های خوبی رو پیدا کنید. همچنین، میتونید کارهایی که ملت روی اون دادهها انجام دادن رو در قالب Kaggle Kernel (به نوعی همون جوپیتر نوتبوک خودمون) ببینید و یا کارهای خودتون هم به اشتراک بذارید.
برای دسترسی به کگل، میتونید روی این لینک کلیک کنید.
Academic Torrents
این وبسایت هم وبسایت جالبیه (و به نوعی مرتبط با بخش بعدی). در واقع هر حرکت آکادمیکی که زده شده و اطلاعاتش هم همزمان منتشر کردند رو در خودش داره. چرا؟ چون جست و جو در محتوای آکادمیک نسبتا سخته و این وبسایت اون کار رو براتون راحت کرده. همچنین یک بخش خوبی برای مجموعهداده (لینک) هم در این وبسایت در نظر گرفته شده.
برای دسترسی به این وبسایت، میتونید از طریق این لینک اقدام کنید.
وبسایت دانشگاهها
همونطوری که در بخش قبلی گفتم، بسیاری از دانشگاهها (و در کل، فضاهای آکادمیک) تحقیقات زیادی انجام میدن و دادههای اون تحقیقات رو هم معمولا منتشر میکنن. چرا که یکی از اصول مطالعات آماری، اینه که دادهها به صورت شفاف منتشر بشن (شاید دلیلش اینه که بعدها، یکی بخواد خودش اون آزمایش و مطالعه رو تکرار کنه و …).
به همین خاطر، وبسایت دانشگاهها – چه ایرانی و چه خارجی – میتونه محل خوبی باشه برای مراجعه و پیدا کردن دادههای خوب برای مطالعه.
دیتاستهای متنباز شرکتها
بسیاری از شرکتهای بزرگ مثل گوگل، فیسبوک، آمازون و …، میان و حجم خوبی از دادههایی که قبلتر در تحقیقاتشون استفاده کردند رو به صورت اوپنسورس، منتشر میکنن. پیدا کردن این دیتاستها هم اصلا کار سختی نیست.
برای مثال، در این لینک میتونید دیتاستهای گوگل رو ببینید. یکی از نمونههایی که خود گوگل اینجا مطرح کرده، دیتاست مرتبط با بیماری کووید-۱۹ است. (لینک)
چرا این شرکتها، دیتاستها رو منتشر میکنن؟ باز هم میگم دقیقا به همون دلیلی که دانشگاهها منتشر میکنن. شاید افراد یا سازمانهایی باشن که بخوان تحقیقات و مطالعات رو برای خودشون تکرار کنند و یا نتیجه آزمایشات و … رو صحتسنجی کنند.
خزیدن (Crawling) صفحات وب
خب، بعضی وقتا هم دادهای که ما نیاز داریم، توسط شرکتها یا دانشگاهها منتشر نشده. پس در این حالت چه کار میکنیم؟ اگر داده مورد نظر، در اینترنت موجود باشه، میتونیم یک خزنده (Crawler) بسازیم و با اون کارمون رو پیش ببریم.
در بسیاری از زبانهای برنامهنویسی و چارچوبهاشون، ابزارهای بسیار خوبی برای کراول کردن صفحات وب وجود داره. یکی از بهترین نمونههاش میتونه BeautifulSoup در پایتون باشه. در مطالب بعدی، احتمالا با استفاده از این ابزار، یک خزنده برای وبسایتهای مختلف خواهیم نوشت.
دوربین، میکروفن، حرکت!
اگر دادههای مورد نیاز ما حتی به شکلی که بتونیم کراول کنیم موجود نبود چی؟ سادهست. ابزارهای ورودی خوبی برای کامپیوتر وجود داره که میتونه بهمون کمک کنه تا داده مورد نظر رو جمعآوری کنید.
گذشته از این، دوربین تلفنهای همراه، میتونه منبع خوبی باشه برای جمع آوری تصاویر(پروژههای بینایی ماشین و …)، میکروفنهای استودیویی برای دریافت صدا خوبن. اگر نیاز به دیتایی مثل گرما یا رطوبت نیاز دارید، طراحی مداری که این داده رو از محیط بخونه و روی دیتابیس خاصی ذخیره کنه کار سختی نیست.
جمعبندی
پروژههای هوش مصنوعی به ذات سخت نیستند. چیزی که اونها رو سخت میکنه، همین دیتای ورودی و تمیزکاری و مرتب کردنشه. بعضی وقتا دادههای ما کم هستند و ما مجبور خواهیم شد که Data augmentation انجام بدیم. بعضی وقتا ممکنه نویز به قدری زیاد باشه که اصلا مرحله جمعآوری دیتا رو مجبور بشیم دوباره از نو انجام بدیم و … .
خلاصه هدف از این مطلب این بود که اگر پا در این عرصه گذاشتید، بدونید همیشه جایی هست که بتونید بدون مشکل، دادههایی رو دریافت و در پروژهتون استفاده کنید و از بابت نویز و …، خیالتون تا حد خوبی راحت باشه.
در مطالب قبلی پیرامون مهندسی اجتماعی نوشتم. در اولین مطلب صرفا به این اشاره کردم که چیه و چرا مهمه، در مطلب دوم در مورد متد ماحیگیری نوشتم و در این مطلب، قراره دو روش دیگر مهندسی اجتماعی یعنی OSINT و همچنین خبرسازی رو با هم بررسی کنیم. اینجا، تقریبا میشه گفت که مباحث مهندسی اجتماعی تمام میشن و احتمالا در آینده جوانب دیگر امنیت سایبری رو با هم بررسی خواهیم کرد.
هوشمندی متنباز (OSINT)
همانطوری که در قسمت اول خدمت شما عرض کردم، ما در شبکههای اجتماعی و در کل در اینترنت خیلی از خودمون ردپا به جا میذاریم. هوشمندی متنباز یا Open Source Intelligence دقیقا همینجا به کار هکرها میاد. روش کلاهسفیدش به نظر میتونه بررسی عقاید و سلایق مردم برای تولید محصول باشه (مثلا خوانندهها به ما پول بدند که در سلیقه موسیقی مردم فضولی کنیم و براشون تحلیل ارائه بدیم) و روش کلاهسیاهش میتونه این باشه که دیتای جمعآوری شده رو بهونهای کنیم برای پروندهسازی و اخاذی از دیگران.
البته ما آدمای خوبی هستیم و چنین کاری نمیکنیم. ساختن پرونده هم به عهده مقامات امنیتی کشورها واگذار میکنیم چون اونها بهتر از ما بلدند و طبیعتا، راحتتر از ما میتونن چنین کاری کنند. اما اگر آدم بدی هستید هم به نظرم بهتره که برید تو اتاق و به کارای زشتتون فکر کنید جای این کارا 🙂
برای OSINT ابزارهای خوبی ساخته شده. در این پست من صرفا ابزارهایی که برای توییتر و اینستاگرام ساخته شدند رو یک بررسی ریز میکنم، اما اگر ابزار دیگری پیدا کردم احتمالا معرفی کنم. حواستون هم باشه استفاده از این ابزارها گرچه مانع قانونی نداره، اما اگر از دادههای به دست آمده برای کاری مثل اخاذی یا پروندهسازی استفاده کنید مسیر رو برای شکایت کردن افراد از خودتان باز خواهید کرد.
توییتر
چند وقت پیش، در توییتر بحثی به نام «ابر کلمات» داغ بود. افرادی که دسترسی به API توییتر داشتند، برای افراد این ابر کلمات رو میساختند. اما من از اونجایی که حوصله نداشتم با توییتر نامهنگاری کنم، دنبال ابزاری بودم که حداقل توییتهای حسابهای حفاظتنشده رو به سادگی بتونه استخراج کنه. اونجا بود که با ابزار twint آشنا شدم (لینک). ابزار twint یا twitter intelligence ابزاریه که به شما کمک میکنه به سادگی در توییتر بچرخید و مثلا توییتهای یک شخص رو استخراج کنید.
برای مثال، برای این که ۱۰۰ توییت آخر ریاست جمهوری آمریکا رو استخراج کنیم، کافیه دستور تویینت رو به این شکل اجرا کنیم:
twint -u potus --limit 100 -o potus.json --json
به این شکل، ما به سادگی به ۱۰۰ توییت آخر اون حساب کاربری دسترسی داریم.
البته میتونیم این دستور رو هی گسترش بدیم و ویژگیهایی مثل تاریخ، ساعت، مکان و … هم بهش اضافه کنیم. حالا این کجا به کارمون میاد؟ یه سناریوی خیلی ساده (که مربوط به بخش دوم همین مطلب هم میتونه بشه) رو در نظر بگیرید. مثلا قراره موز گران بشه. شما کافیه که twint رو به این شکل اجرا کنید:
twint -s "گرانی موز" --near Tehran --limit 1000
این هزار توییت آخری که مربوط به «گرانی موز» میشن رو برای ما لیست میکنه. میتونیم بفهمیم چه کسانی بهش دامن زدند و چه کسانی پیروی کورکورانه کردند و … . خلاصه که اتصال نقاط به یکدیگر خیلی راحتتر میشه.
اینستاگرام
برای اینستاگرام هم ابزارهایی وجود دارند که کمک کنند شما به سادگی بتونید در دیتایی که مردم به صورت حفاظتنشده (پابلیک یا عمومی) منتشر کردند بخزید و ببینید که دنیا دست کیه. OSINT در اینستاگرام، علاوه بر مقاصد خبیث (😂) میتونه به شدت بهتر برای مقاصد بازاریابی و تجاری استفاده بشه. چرا که بسیاری از مردم، در اینستاگرام به دنبال چیزایی که دوست دارند میگردند و خیلیها هم حتی خریداشون رو از طریق اینستاگرام انجام میدن.
البته، اینستاگرام الگوریتمهای عجیب و غریب زیاد داره و جدیدا APIش هم کمی سختگیر تر شده. ابزاری که برای اینستاگرام پیدا کردم، اسمش هست Osintgram (لینک) و این ابزار رو متاسفانه فرصت نشده که تست کنم هنوز. اما، یک ویدئوی خوب از کانال NetworkChuck براش پیدا کردم که میتونید اینجا ببینیدش.
ابزارهای دیگر؟
صد در صد هزاران ابزار دیگر برای OSINT وجود دارند. من فقط میخواستم که مفاهیم پایهش رو با هم بررسی کنیم و ببینیم که چی به چیه. در آینده، اگر ابزار خوبی برای OSINT پیدا کنم، حتما معرفی خواهم کرد چرا که لازمه بدونیم در دنیای اینترنت چطور میتونیم به سادگی تحت نظر باشیم. اونم نه تحت نظر افرادی که با تحت نظر گرفتن آدما، امنیتشون رو تامین میکنند، تحت نظر یه مشت دیوانه 🙂
خبرسازی
خبرسازی، یکی از راههای دیگر مهندسی اجتماعی، برای جا انداختن یک مفهوم یا یک ترس یا حتی طرفداری از شخصه. خبرسازیها، معمولا از طرف یک نفر نیستند بلکه از طرف گروهها انجام میشن. مثال کلاسیکش هم میتونه ماجرای فیسبوک و رباتهای روسی در جریان انتخابات امریکا باشه.حالا این خبرسازیها به چه شکل صورت میگیره؟ معمولا یک الگوریتم خاصی رو داره. به جهت این که مثالش کمی ملموستر باشه، از ترند «آهنربایی شدن بدن بعد دریافت واکسن» که تازگیها خیلی روش مانور داده شد استفاده میکنم.
مرحله اول: یک نفر به قصد شوخی در پلتفرمهای شوخی مثل تیکتاک، ویدئویی میذاره از این که اشیا فلزی بعد دریافت واکسن بهش میچسبن (لینک)
مرحله دوم: افرادی که میدونن این موضوع شوخیه، اون رو در شبکههای اجتماعی به نمایش میذارند.
مرحله سوم: اشخاصی که از ابتدا با موضوع واکسن مشکل داشتند، با زدن سر و ته (دقت کنید زدن سر و ته اینجا واقعا مهمه!) داستان، اون رو بازنشر میدن.
مرحله چهارم: توهمش در بسیاری از افراد به وجود میاد و یک سری خبر واقعیتر و غیرتیکتاکی ساخته میشه (لینک)
مرحله پایانی: افراد زیادی باور میکنند که چنین چیزی وجود داره …
حالا این خبر، از خبرهای میشه گفت شکبرانگیز بوده. اما در همان مثال «گرانی موز» هم میشه اینطور خبرسازی کرد. البته خبرسازی معمولا روشهای جلوگیری خوبی هم داره که در ادامه بهشون میپردازیم.
چطور بفهمیم که قربانی خبرسازی نشدیم؟
معمولا ما سواد رسانهای بالایی نداریم. چون کارمون رسانه نیست. به همین خاطره که به راحتی میتونیم قربانی خبرسازی و اخبار جعلی – بخصوص در محیط شبکههای اجتماعی – بشیم. اول از همه زمانهای بسیار قدیم یادمه که وبسایت درسنامه دورهای برای این کار داشت اما متاسفانه ظاهرا این وبسایت دیگر در دسترس نیست (و در ویکیپدیا هم توضیح درستی نیست ازش) اما خب میشه با یک سری راه ساده فهمید که داستان چیه.
بپرسید. اگر افرادی رو میشناسید که در اون حوزه فعالن حتما بپرسید.
بررسی کنید. با همین ابزارهای OSINT میتونید خط فکری و طرفداری افرادی که یک خبر رو پخش کردند پیدا کنید. این به شما در قضاوت بهتر کمک میکنه.
واکنشسنجی کنید. واکنش مردم در این موارد واقعا مهمه، ببینید که اکثریت چه واکنشی میتونن نشون بدن. عمدتا این واکنشها البته درست نیستند، اما میتونن سرنخهای خوبی به من و شما بدند.
در نهایت، از منابع خبری معتبر داخلی و خارجی استفاده کنید تا صحت خبر را بسنجید.
شاید این آخرین مطلب از سری اختصاصی «مهندسی اجتماعی» بود، اما باید یادمون باشه که همیشه این روشها یکسان نیستند و در طول زمان میتونن بهروز بشن. در واقع یادتون باشه، هر قفلی که ساخته بشه، یک قفلشکن هم براش پیشتر ساخته شده.
وظیفه ما، اینه که هم دیگر رو آگاه کنیم و به هم بگیم که چه چیزی ممکنه به ما در اینترنت آسیب بزنه یا در کل، ما رو در اینترنت عریانتر کنه. معرفی ابزارها و روشها، صرفا به این خاطره که شما بتونید خودتون رو بررسی کنید و ببینید که چقدر دیتا ازتون موجوده و اگر راضی نیستید به وجودش، حتما برای حذفشون اقدام کنید.
خلاصه که متشکرم از وقتی که برای خوندن این مطلب گذاشتید. امیدوارم مفید واقع شده باشه.
پس از مدت طولانی، با یک پست دیگر برگشتم و این بار قراره با هم احراز هویت JWT رو در ریلز بررسی کنیم. اول از همه، لازمه بدونیم JWT چیه؟ چرا نیازش داریم؟ اصلا چرا احراز هویت و هزاران چرای دیگر که احتمالا الان در سر شما هستند. بعدش یک پروژه خیلی کوچولو ایجاد میکنیم و با هم براش احراز هویت رو پیادهسازی میکنیم 🙂
احراز هویت JWT چیه و چرا بهش نیاز داریم؟
اول از همه این سوال بنیادیتر رو پاسخ بدیم که «چرا احراز هویت نیاز داریم؟» و بعد بریم سراغ احراز هویت JWT که قراره در این مطلب در موردش مفصل حرف بزنیم.
ما به احراز هویت نیاز داریم چون همیشه بخشی از دادههای ما، خصوصی هستند. گذشته از اون، احراز هویت میتونه اجازه CRUD رو به شما بده، نه؟ فکر کنید اپی دارید که هرکسی میتونه روش بخونه و بنویسه. ممکنه خوندن چیزی باشه که برای «همه» مناسب باشه اما «نوشتن» اینطور نیست. بخصوص که نوشتن خودش به قسمتهایی مثل حذف و بروزرسانی هم شکسته شده.
پس ما به احراز هویت نیاز داریم که هر ننهقمری (😂) نتونه از API ما استفاده کنه. بلکه کاربرانی که ثبتنام کردند و دسترسی درستی به سرویس دارند، بتونن استفاده کنن. این قضیه در API های تجاری (یا Business facing) خودشون رو بیشتر و بهتر نشون میدن.
حالا سوال مهمتر …
احراز هویت JWT چیه؟
در اپهای قدیمی (مثل همین وردپرس)، احراز هویت توسط cookie ها انجام میشه. به چه صورتی؟ به این صورت که وقتی نام کاربری و گذرواژه وارد میکنیم، نرمافزار فضایی رو در مرورگر به خودش اختصاص میده و یک سری اطلاعات رو با خودش جابجا میکنه. اما در ReST API ها این قضیه رو نداریم. در واقع نمیتونیم به کوکیها اعتماد کنیم. پس چه میکنیم؟ اینجا لازمه جز یوزرنیم(که معمولا میتونه عمومی باشه) و پسورد (که میتونه راحت لو بره) میاییم و یک «توکن» هم تعریف میکنیم. این توکن، میتونه ثابت یا متغیر باشه. یعنی چی؟ یعنی میتونه به ازای هربار ورود تغییر کنه، میتونه سر زمان مشخصی هم منقضی بشه.
حالا توکن چیه؟ توکن به صورت کلی، در کازینوها معادل پولیه که شما در بازیها قرار میدید، در واقع مجوز حضور شما در اون کازینو، کلاب و … است. حتی به رمزارزهایی که بر مبنای دیگر رمزارزها ساخته میشن هم سکه نمیگن بلکه میگن توکن. شما فرض کنید که مثلا ۱۰۰ دلار میدید و پنج تا سکه با آرم اون کازینوی خاص دریافت میکنید. اگر در بازی برنده بشید یا ببازید، باید توکنهاتون رو تحویل بدید یا بگیرید.
حالا در احراز هویت JWT هم، ما به ازای کاربرمون یک توکن در نظر میگیریم. این توکن، معمولا یک رشته طولانیه که انسان نمیتونه بخوندش. نتیجتا خیلی از اطلاعات ما به صورت ایمنتر میتونن رد و بدل بشن (طبیعیه که مواردی مثل SSL داشتن و الگوریتمهایی که در ساخت توکن داشتیم هم مهمن). ضمن این که نامکاربری، ایمیل، رمزعبور و .. هم به همین سادگیا نمیتونن خونده بشن.
پس ما میآییم و یک دیتابیسی از توکنها در کنار دیتابیسی از یوزرها میسازیم (البته درستترش، جدوله!) و به ازای هر یوزر معمولا دوتا توکن در اون دیتابیس قرار میدیم. یکیش رو بهش میگیم «توکن دسترسی» یا Access Token و یکی رو میگیم «توکن بازنشانی» یا Refresh Token. توکن دسترسی، معمولا یک تاریخ انقضایی داره و بعد از اون با استفاده از توکن بازنشانی، میتونیم یکی جدید بگیریم. اما در آموزش امروز، صرفا میخوایم توکن دسترسی رو به دست بیاریم.
احراز هویت JWT به این شکل کار میکنه
خب، الان که تقریبا همهچی رو میدونیم، بریم برای پیادهسازی.
پیادهسازی یک اپلیکیشن ریلز با JWT
خب در قدم اول، باید یک اپ ایجاد کنیم. این اپ رو به این شکل ایجاد میکنیم:
rails new devise-jwt --api
خب توضیح واضحات:
قسمت rails که واضحا فراخوانی نرمافزار rails در ترمینال ماست.
قسمت new در خواست برای ایجاد یک اپ جدیده.
قسمت devise-jwt اسم پروژه ماست. حالا چرا؟ چون قراره از یک لایبرری با همین اسم استفاده کنیم. بنابراین، پروژه رو اینطوری اسم گذاشتیم.
در قسمت آخر هم، به ریلز گفتیم که ما تو رو بخاطر API هات دوست داریم. ویو نیاز نیست.
بعد از چند ثانیه (و بسته به سرعت اینترنت دقیقه) اپ ما ساخته میشه. بعد لازمه که مرحله مرحله کارهایی رو انجام بدیم.
نصب لایبرریهای مورد نیاز
خب، اول از همه با ویرایشگر متنی مورد علاقمون فایل Gemfile رو باز میکنیم و این خطوط رو بهش اضافه میکنیم :
gem 'devise'
gem 'devise-jwt'
gem 'rack-cors'
بعد از این که این خطوط رو اضافه کردیم، دستور زیر رو اجرا میکنیم:
bundle
این دستور چه کار میکنه؟ میاد و تمام لایبرریهای مورد نظر شما رو به صورت ایزوله در یک دایرکتوری، نصب میکنه. به این شکل شما میتونید به سادگی بدون رسیدن آسیب به باقی لایبرریهای روبی نصب شده روی سرور یا حتی کامپیوتر خودتون، ایدههاتون رو تست کنید.
لازم به ذکره که بعد از اجرای این دستور فایل Gemfile.lock بهروز میشه، این فایل حالا چه کار میکنه؟ این فایل حواسش به همهچی هست. در واقع، ورژن روبی، ورژن ریلز، لایبرریهای مورد نیاز و ورژنینگشون و … رو همه رو این فایل داره کنترل میکنه.
بعد از انجام مراحل فوق، کافیه این دستور هم اجرا کنیم:
rails g devise:install
این دستور چه میکنه؟ این دستور هم برای ما فایلهای devise رو در جای درستش قرار میده.
آشنایی با devise
برای احراز هویت در هر سیستمی ما دو راه داریم:
نوشتن سیستم احراز هویت از بیخ
استفاده از کتابخانههای موجود
در مورد روش «از بیخ»، ما معمولا این کار رو انجام نمیدیم. چرا؟ چون معمولا اونقدر خوب نیستیم که بتونیم امنیت سیستم رو به خوبی تامین کنیم. در مورد دوم، در هر فرمورک و زبانی، کتابخانههایی ساخته شدند که کمک میکنن ما بتونیم با اضافه کردنشون به پروژه خودمون، بخش احراز هویت رو هندل کنیم. برای ریلز devise ساخته شده. این لایبرری، یک لایبرری مبتنی بر cookie برای احراز هویت وباپهاست.
بعد از همهگیر شدن ReST API ها، لایبرری devise-jwt هم نوشته شد. این لایبرری، ابزاریه که به من و شما کمک میکنه بتونیم احراز هویت JWT رو به پروژهمون اضافه کنیم. در واقع هر سه لایبرری که به پروژه اضافه کردیم، کارشون همینه که JWT رو برای ما راحت کنند.
هندل کردن CORS
در این مطلب قصد ندارم در مورد CORS حرف بزنم، چون قبلتر ازش حرف زدم (و میتونید اینجا بخونید). اینجا ما صرفا قصدمون اینه که بیاییم و این مشکل رو حل کنیم. چطوری؟ خب این فایل:
config/initializers/cors.rb
رو با ویرایشگر متنی مورد علاقهمون باز میکنیم، و محتواش رو به این شکل تغییر میدیم:
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins '*'
resource '*',
headers: :any,
methods: [:get, :post, :put, :patch, :delete, :options, :head]
end
end
توجه کنید که این قسمت معمولا به صورت کامنتشده در کد هست، فقط کافیه آنکامنتش کنید و نیاز نیست که کلا این مورد رو کپی پیست کنید.
ساخت مدل و کنترلرهای مورد نیاز برای کاربر
یکی از خوبیهای devise اینه که به شکل scaffold گونهای، میتونه به ما کمک کنه که کاربر و سیستم کنترلش رو بسازیم. برای ساخت مدل کاربر فقط کافیه که این دستور رو اجرا کنیم:
rails g devise User
به این شکل میفهمه که باید یک مدل، مطابق مدل User ولی با مشخصات devise برامون بسازه. بعدش هم کافیه این دستور رو اجرا کنیم:
rails db:migrate
که جدولای مرتبط برامون در دیتابیس ساخته بشن.
حالا که خیالمون از بابت این قضایا راحت شد چی؟ هیچی. دو تا کنترلر هم میسازیم به این شکل:
rails g controller users/sessions
rails g controller users/registrations
بعد از این میشه گفت که کار ما اینجا تمام شده و باید بریم یه چیزایی رو ادیت کنیم 🙂
ویرایش مدل یوزر
بعد از این که کارهای بالا رو انجام دادیم، کافیه که بریم سراغ مدل یوزرمون و به این شکل ادیتش کنیم:
class User < ApplicationRecord
devise :database_authenticatable,
:jwt_authenticatable,
:registerable,
jwt_revocation_strategy: JwtDenylist
end
حالا این کار برای چیه؟ برای اینه که ما یک جدول دیگر به اسم JWT Deny List در نظر میگیریم و توکنهای منقضیشده رو درونش قرار میدیم. به اون شکل وقتی توکنی منقضی بشه، میتونیم به کاربر خطا نشون بدیم یا از توسعهدهندههای فرانت تیم بخوایم که وقتی اون خطا رو دیدن، کاربر رو لاگ اوت کنن. خلاصه که راه برای رسیدن به نتیجه مطلوب زیاده. بگذریم، بعد از این، در پوشه مدلها لازمه که یک فایل به اسم jwt_denylist.rb ایجاد کنیم و این محتوا رو درونش قرار بدیم:
class JwtDenylist < ApplicationRecord
include Devise::JWT::RevocationStrategies::Denylist
self.table_name = 'jwt_denylist'
end
بعد نیاز داریم که برای این قضیه یک مایگرشن اضافه کنیم:
rails g migration CreateJwtDenylist
سپس، فایل مایگرشن که معمولا در آدرس:
db/migrate
قرار داره رو باز میکنیم و محتواش رو به این شکل تغییر میدیم:
class CreateJwtDenylist < ActiveRecord::Migration[6.1]
def change
create_table :jwt_denylist do |t|
t.string :jti, null: false
t.datetime :exp, null: false
end
add_index :jwt_denylist, :jti
end
end
و بعد یک دور مایگرشنها رو اجرا میکنیم:
rails db:migrate
تا اینجا مطلب طولانی شد؟ ایرادی نداره. بریم یک قهوه بزنیم به بدن و برگردیم 🙂
کنترلر Session
امیدوارم که کافئین به قدر کافی مودتون رو بالا آورده باشه 🙂 حالا وقتشه که بریم و کنترلر session رو درست کنیم. نیازی نیست واقعا کار خاصی کنیم. تنها کاری که نیازه بکنیم اینه که کنترلری که ساختیم رو باز کنیم و این موارد رو درش کپی کنیم:
class Users::SessionsController < Devise::SessionsController
respond_to :json
private
def respond_with(resource, _opts = {})
render json: { message: 'You are logged in.' }, status: :ok
end
def respond_to_on_destroy
log_out_success && return if current_user
log_out_failure
end
def log_out_success
render json: { message: "You are logged out." }, status: :ok
end
def log_out_failure
render json: { message: "Hmm nothing happened."}, status: :unauthorized
end
end
نکته مهم، اگر هنگام ساخت کنترلر، جای users از چیز دیگری استفاده کردید باید Users رو در کد بالا به اون تغییر بدید. اگر هم کلا چیزی نذاشتید، کل قسمت Users:: رو ازش حذف کنید.
کنترلر Registration
خب عین همون بخش قبلی، شما کافیه کنترلر registrations رو باز کنید و این کد رو درونش کپی کنید:
class Users::RegistrationsController < Devise::RegistrationsController
respond_to :json
private
def respond_with(resource, _opts = {})
register_success && return if resource.persisted?
register_failed
end
def register_success
render json: { message: 'Signed up sucessfully.' }
end
def register_failed
render json: { message: "Something went wrong." }
end
end
تنظیمات نهایی devise
خب اول در ترمینال (یا cmd) این دستور رو اجرا کنید:
rake secret
و یک کد طولانی و مسخره بهتون میده 😁 اون رو در فایل:
config/initializers/devise.rb
در آخر فایل به این شکل کپی کنید:
config.jwt do |jwt|
jwt.secret = rake_secret_output
end
نکته بسیار مهم اینجا چیه؟ این که حواستون باشه این صرفا یک پروژه تسته و برای محیط پروداکشن اصلا جالب نیست که سیکرتها و توکنها، هاردکد باشن. برای اون زمان میتونید از ENV استفاده کنید.
مسیرها
خب، الان که تقریبا همهچی آرومه و ما چقدر خوشحالیم، کافیه که بیاییم و فایل routes.rb رو هم به این شکل ویرایش کنیم:
Rails.application.routes.draw do
devise_for :users,
controllers: {
sessions: 'users/sessions',
registrations: 'users/registrations'
}
end
خب پس چی میمونه که انجام ندادیم؟ یک سری آزمایش ساده 🙂
ساخت کاربر
خب الان کافیه بعد اجرای سرور ریلز (مطابق آموزشهای قبلی)، این دستور رو اجرا کنیم:
که اینها سرایند (Header) های ما هستند. در این قسمت، هرچی جلوی Authorization قرار داره توکن ماست. و میتونیم ازش استفاده کنیم.
بخش بعدی هم اینه :
{"message":"You are logged in."}
که صرفا به ما میگه ورودمون موفقیتآمیز بوده.
فکر کنم این مطلب، آخرین مطلبی بود که در مورد بیسیکهای روبیآنریلز مینوشتم. احتمالا در آینده نهچندان دور، همه اینها رو با هم به یک سری آموزش ویدئویی تبدیل کنم و از طریق آپارات یا یوتوب منتشرشون کنم.
طبیعتا یک قسمتهایی از آموزش در این مطلب پوشش داده نشده، سعی میکنم در آینده یک یا دو پست تکمیلی هم ارائه کنم که همه این قضایا به خوبی پوشش داده بشه (یا این که کلا در بخش ویدئویی در خدمتتون باشم).
به صورت کلی، دوست دارم بدونم نظر شما در مورد این تیپ آموزشها چیه؟ آیا ادامهشون بدم یا خیر؟ و این که آیا پایهش هستید که بحث فرانتند رو هم شروع کنیم یا روی همین بکند باقی بمونیم و اول یه پروژه کامل رو بکندش رو بزنیم و بعد بریم سراغ فرانت؟ 🙂
در آخر هم بابت وقتی که گذاشتید و مطلب رو خوندید ازتون متشکرم.
در پست قبلی با هم یک REST API نوشتیم که یک نمونه از مدل «پست» رو میتونست ایجاد کنه، بروز کنه، نمایش بده و نابود کنه.
در این پست هم قصد دارم که کار مشابهی کنم، منتها این بار تیم محصول به ما یک سناریوی جدید داده. از ما خواستند که این بار، کامنت هم به پستها اضافه کنیم و بتونیم با استفاده از API برای هرپستی، یک کامنت هم ایجاد کنیم. پس وقت رو غنیمت میشماریم و میریم سروقت پروژه (پوشهها و ساختار عینا مثل پست قبلیه و هیچ تغییری در اون رخ نداده).
ساخت مدل برای کامنت
بیاییم ببینیم تیم محصول برای ما چی طراحی کرده. این عزیزان در نظر دارند که هر کامنت صرفا یک «متن بدنه» داشته باشه و بیشتر از اون نیاز نداشتند. کار ما اینه که حالا مدلی طراحی کنیم که علاوه بر اون، به پستها هم ربط داشتهباشه. چطوری این کار رو میتونیم بکنیم؟
کافیه دستور زیر رو اجرا کنیم و مدلش رو بسازیم:
rails generate model Comment body:text post_id:integer
خب حالا میریم به پوشه :
app/models
و اول post.rb رو به این شکل ویرایش میکنیم:
class Post < ApplicationRecord
has_many :comments
end
و سپس comment.rb رو به این شکل ویرایش میکنیم:
class Comment < ApplicationRecord
belongs_to :post
end
حالا این خطوط چی میگن؟ ما در پایگاه داده چندین نوع رابطه داریم. توضیح این روابط به صورت مفصل باشه برای یک پست دیگه. اما اینجا بیاید در نظر بگیرید که طراحی محصول به شکلی بوده که «هر پست میتونه بیشمار کامنت داشته باشه و هر کامنت متعلق به فقط و تنها فقط یک پسته». این نوع رابطه اسمش هست «یک به چند» یا بهتر بگم «یک به خیلی» و به قول خارجیها One to many.
حالا که از این قضیه خبر داریم و پست رو هم ساختیم تعلل نمیکنیم. میریم سراغ ساخت کنترلر مربوطه. اینجا کنترلر به ما کمک میکنه که بتونیم به سادگی یک کامنت رو روی پست بسازیم و مدیریت کنیم. در مورد سناریوی کنترل کامنت هم این بار کمی سادهتر میگیریم. در ادامه این مورد رو با هم بررسی خواهیم کرد.
ساخت کنترلر کامنت
اول کنترلر کامنت رو به این شکل ایجاد میکنیم:
rails generate controller api/v1/comments
و سپس در فایل:
config/routes.rb
این تغییر ریز رو ایجاد میکنیم :
Rails.application.routes.draw do
namespace :api do
namespace :v1 do
resources :post do
resources :comments
end
end
end
# For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
end
و بعد میریم سروقت کنترلر 🙂 اما قبل از اون بیاید یه چیزی رو بررسی کنیم. این روابط رو! این روابط چطوری تعیین شدند؟ و چرا مهمند. پس در ترمینالمون تایپ میکنیم:
rails c
این دستور، به ما یک «کنسول ریلز» میده. کنسول به ما کمک میکنه که ایدهها رو در یک لول قبل از مرورگر و درخواستهای HTTP بررسی کنیم. در اسکرینشات زیر از ترمینال من، میبینید که چطوری یکی از پستها رو تست کردم و دیدم که آیا روابطش با کامنتها درسته یا خیر.
خب حالا بیاییم کنترلر رو بنویسیم. قبل از اون، من یک کامنت دستی در کنسول به این شکل میسازم:
c = Comment.new(:body => "This is a comment", :post_id => 1)
c.save
و کنسول رو میبندم. میریم سراغ کنترلرمون. همونطور که گفتم اینجا یه سری چیزا نیستن. مثلا show اینجا نیازی نیست باشه ولی index نیاز هست. پس میریم سراغ این که این موارد رو در کنترلر لحاظ کنیم. خب با توجه به این توضیحات، ما یک متد index به این شکل نیاز داریم:
def index
@post = Post.find(params[:post_id])
render json: @post.comments
end
همونطور که دیدید، اینجا تنها چیزی که نیازه بررسی بشه post_id ماست. درخواستی که به سمت سرور میفرستیم هم به این شکله:
curl -X GET -i http://localhost:3000/api/v1/post/1/comments
حالا وقتشه که بتونیم یک کامنت جدید ایجاد کنیم. برای ساخت کامنت جدید هم کافیه که به این شکل، متد create رو بنویسیم:
def create
@comment = Comment.new(:body => params[:comment][:body], :post_id => params[:post_id])
if @comment.save
render json: {:status => "success", :comment => @comment}
end
end
و نمونه درخواستی که براش میفرستیم هم به این شکل:
curl -X POST -H 'Content-Type: application/json' -i http://localhost:3000/api/v1/post/1/comments --data '{
"body":"This is another comment"
}'
حالا یک سوال مهم ممکنه برای شما پیش بیاد و اون هم اینه که :
چرا پارامترهای ارسالی فرق دارند؟
دلیلش خیلی سادهست. ریلز اول میاد پارامترهای درون URL رو مستقیم میخونه، بعد میاد سراغ Request Body که در واقع اگر سلسله مراتبی (مثل یک فایل YAML) بهش نگاه کنیم این شکلی میشه:
- post_id: 1
- comment:
- body: "This is another comment"
در واقع برای خودش یک Resource space در نظر میگیره و body رو از اون میخونه. به همین خاطره که یکی زیرمجموعه comment و دیگری مستقیما post_id میشه.
باقی متدها چی؟
معمولا کامنتها قابل ادیت و حذف و … نیستند. ما هم به جهت سادگی این ماجرا رو براشون پیادهسازی نمیکنیم تا بعدا چه پیش آید 🙂
جمعبندی دو قسمت اخیر
خب در این دوقسمت ما خیلی چیزا یاد گرفتیم که فهرستوار بررسی میکنیم :
چطور ریلز نصب کنیم.
چطور یک پروژه ریلز جدید ایجاد کنیم.
چطور یک منطق تجاری (Business Logic) رو درک کنیم
چطور مدلهای مورد نظر رو بسازیم
چطور API بسازیم و تست کنیم.
این موارد بسیار مهمن و فکر کنم بعد خوندن این دو قسمت حداقلهای ساخت یک API رو یاد گرفتید. بعد از این چه چیزهایی لازمه که یاد بگیریم؟ این دیگه بستگی به خودتون داره. شاید در موردش مطلبی بنویسم اما فکر کنم این آخرین مطلبیه که انقدر مستقیم داره به نوشتن و ساختن API اشاره میکنه.
در مطالب بعدی، میخواهیم بریم سراغ یک سری مفهوم و پیادهسازی دیگر. احتمال قوی هم بریم سراغ فرانتند و ببینیم که در دنیای فرانتند چه خبره. پس منتظر باشید که فصل جدیدی از مطالب فنی در راهه 🙂
در آخر، از شما بابت وقتی که برای خوندن این مطلب گذاشتید هم کمال تشکر رو دارم.
پس از مدتی دست کشیدن از بلاگ نوشتن، به دنیای نویسندگی فنی برگشتم و قراره که امروز با هم یاد بگیریم چطور یک Rest API رو با روبی آن ریلز بسازیم. چرا روبی آن ریلز؟ دو تا دلیل داره. یکی این که چون من بلدمش و دلیل دوم این که منبع فارسی خوب براش به شدت کمه (کما این که برنامهنویس خوب ریلز کم نداریم در ایران).
در این پست قراره چیا یاد بگیریم؟ اول این که Rest چیه. بعدش با ریلز یه پروژه جدید ایجاد میکنیم، بعدش هم یک سناریوی ریزی که در این قسمت تا بخش خوبیش رو پیش میبریم. در قسمت بعدی هم پروژه رو تموم میکنیم و میریم که داشته باشیم فرانتند رو 🙂
تعریف Rest
واژه REST مخفف Representational State Transfer و به معنای «انتقال بازنمودی حالت» است. یعنی چی؟ یعنی ساده و مختصر و مفیدش اینه که شما هرچی که دارید رو بدون تغییر در حالاتش، به کاربر منتقل میکنید. یک تعریف بهترش هم اینه که همه چی به شکل یک لینک اینترنتی و خروجی قابل فهم برای مرورگر (و البته بهتر بگم، پروتکل HTTP) دربیاد.
رست، مزایای خاص خودش هم داره. اما بزرگترین مزیتش اینه که همهجا یکیه! یعنی اینطوری نیست که روی وبسایت من یه شکل باشه و روی یک وبسایت دیگه یه شکل دیگه. همهجا میتونید با دانشی که از HTTP و ابزارهایی که برای ارسال و دریافت درخواست HTTP دارید، باهاش کار کنید. ابزاری که در این پست باهاش کار میکنیم چیزی نیست جز curl دوست داشتنی. یک ابزار خطفرمانی در یونیکس که سالیان ساله برای هر بده بستانی با HTTP داره استفاده میشه.
نصب و راهاندازی روبی، ریلز و نود جی اس!
نصب کردن این موارد حقیقتا چیزی نیست که بخوام در این مطلب بهشون اشاره کنم، فقط یک سرنخ کوتاه میدم و شما خودتون دنبالش کنید.
همونطور که از اسم فرمورک مورد نظر پیداست، یک چارچوب توسعه روی زبان روبی محسوب میشه. حالا چطوری روبی رو نصب کنیم؟ یک راه نسبتا بدی هست که از مخازن توزیع یا brew نصب کنیم، یک راه بهتر هم استفاده از RVM میتونه باشه. شخصا همیشه از RVM استفاده میکنم، و راضی هم هستم. چون بهتون اجازه میده که چندین و چند نسخه روبی نصب داشته باشید.
برای نصب RVM از وبسایتش (لینک) استفاده کنید. این ابزار روی لینوکس و مک به راحتی نصب میشه. برای نصب روی ویندوز هم من از همین ابزار به کمک WSL استفاده کرده بودم و نتیجه، رضایت بخش بود. بعد از نصب این بزرگوار مطابق راهنماهاش، یک نسخه روبی (پیشنهاد من ۲.۷.۲ که این مطلب با استناد به استانداردهاش نوشته شده) نصب کنید. بعد از نصب روبی، این دو دستور رو اجرا کنید که ریلز و متعلقات نصب بشن:
gem install bundle
gem install rails
نصب ریلز یکمی طولانیه. میتونید این زمان رو صرف نوشیدن یک فنجان قهوه کنید 🙂 این طولانی بودن هم ربط زیادی به اینترنت نداره، چون روی بستههای خاصی مثل sass یکم درجا میزنه چون نیاز داره یه چیزایی رو کامپایل کنه.
بعد از این، شما با مراجعه به وبسایت نود جی اس (لینک) و yarn (لینک) میتونید اینها رو هم متناسب با سیستمعامل خودتون نصب کنید. حالا همه چی آمادهست که پروژهمون رو بزنیم 🙂
آغاز پروژه
سناریو
خب بیاید فرض کنیم که یک شرکتی قراره یه سرویس وبلاگدهی مشابه مدیوم یا ویرگول ارائه کنه. در حال حاضر هیچی ازمون نخواسته جز :
امکان ارسال پست از طریق API
امکان ارسال نظر روی پست از طریق API
و خب در حال حاضر احراز هویت و … براشون مهم نیست چون میخوان ببینن که MVP کار میکنه یا نه؟! و ما هم از خداخواسته قبول کردیم این رو براشون پیاده کنیم. اما این همه ماجرا نیست. این دو مورد، باید یک سری ویژگی هم داشته باشن. این ویژگیها رو طراح محصول باید درآورده باشه. فرض کنیم که طراح محصول اینا رو درآورده و فعلا برای «پست» به ما یک سری ویژگی داده :
عنوان یا title : یک رشته کرکتری
عنوان کوتاه یا slug : یک رشته کرکتری که با – از هم جدا میشه، یک رشته رندم هم تهش میچسبه که منحصر به فرد بودنش رو تعیین کنه (چسبیدن رشته رندم رو از فرانت هندل کنید)
بدنه یا body : متن اصلی.
خب، با این ویژگیها از ما میخوان که پست رو پیاده کنیم تا ویژگیهای کامنت رو بهمون بدن 🙂
ساخت پروژه جدید
ساخت پروژه جدید در ریلز بسیار سادهست. شما فقط کافیه که در پوشهای (مثلا من روی کامپیوتر یه پوشه دارم به اسم playground که پروژههای شخصیم توشن) که میخواید پروژه اونجا باشه، این دستور رو اجرا کنید:
rails new api-tutorial --api
اینجا چی کار میکنیم؟ rails که مشخصه، داره rails رو فراخوانی میکنه. new میگه یک پروژه جدید میخوام. قسمت بعدی، اسم پروژه و طبیعتا اسم پوشه پروژهست. در نهایت بخش مهم همون api عه. این داره به ریلز میگه که viewها و کلا مسخرهبازیای فرانتی رو بعدا و جدا انجام میدم. فعلا مسخرهبازی بکندی میخوام فقط 😁
حالا با اجرای دستور زیر، میریم به داخل پوشه پروژهمون:
cd api-tutorial
ساخت مدل جدید برای پست
همونطور که از مطلب MVC (لینک) میدونیم، مدل تعیین میکنه که یک موجودیت در دیتابیس به چه شکل قرار بگیره. خب، الان کاری که ما باید بکنیم چیه؟ اینه که دستور زیر رو اجرا کنیم و بگیم که چی میخوایم:
rails generate model Post title:string slug:string body:text
خب، در اینجا دقیقا همون چیزی که طراح محصول خواسته رو داریم پیادهسازی میکنیم. خب، الان در پوشه:
app/models
یک فایل به اسم post.rb ساخته شده که توش تقریبا هیچی نیست. و فعلا به اون فایل، کاری نداریم (تو قسمت دوم در مورد این فایل صحبت خواهیم کرد).
الان فقط لازمه که به دیتابیس بفهمونیم که ما یک جدول جدید برای پستها نیاز داریم. برای اون کار، دستور زیر رو اجرا میکنیم:
rails db:migrate
این کار برای ما جدول و روابطش رو میسازه.
ساخت کنترلر برای پست
خب، ما نیاز داریم که عملیات CRUD روی پست انجام بدیم. چرا؟
هر پست نیاز داره ایجاد بشه. پستی که ایجاد نشه پستیه که قطعا ایجاد نشده 😐
هر پست نیازمند اینه که به کاربر نمایش داده شه. قرار نیست مخزنالاسرار بسازیم که!
هر پست نیاز داره که ویرایش بشه. فرض کنید یه جا تو پست نوشتیم «خورش کرفس بهترین غذای دنیاست». خب معلومه که باید «خورش کرفس» رو به «قرمه سبزی» تغییر بدیم.
و هر پست باید قابل حذف شدن باشه. در مورد حذف نرم و سخت بعدا صحبت خواهیم کرد. اما فرض رو روی destroy یا همون «حذف سخت» میذاریم. چون طراح محصول هنوز اطلاعات بیشتری بهمون نداده.
خب، الان که این رو یاد گرفتیم چی کار میکنیم؟ این دستور رو اجرا میکنیم:
rails generate controller api/v1/post index show create update destroy
بعدش فایل :
app/controllers/api/v1/post_controller.rb
رو باز کنید. با چنین صحنهای روبرو خواهید شد:
class Api::V1::PostController < ApplicationController
def index
end
def show
end
def create
end
def update
end
def destroy
end
end
خب اول از new شروع میکنیم! از حرف C ! اما قبلش باید یک تغییر کوچکی در این فایل بدیم :
class Api::V1::PostController < ApplicationController
def index
end
def show
end
def new
end
def update
end
def destroy
end
private
def post_params
params.require(:post).permit(:title, :slug, :body)
end
end
متد post_params چی کار میکنه؟ این متد میاد فقط مواردی که نیازن رو validate میکنه و نمیذاره پارامتر بیشتر و یا اشتباهی به endpoint مربوطه پاس بدیم. خب، الان متد new رو به این شکل تغییر میدیم:
def new
@post = Post.new(post_params)
if @post.save
render json: {:status => "success", :content => @post}
else
render error: {:status => "fail"}
end
end
خب یک مزیت دیگر استفاده از post_params هم همینه، بدون دردسر میتونیم همه پارامترها رو پاس بدیم به متدهامون. حالا، بهتر اینه که این متد رو تست هم بکنیم نه؟ پس این دستور رو وارد میکنیم:
rails server
این دستور، سرور ریلز رو ران میکنه و به ما اجازه میده که برنامه خودمون رو آزمایش کنیم.
اما الان بهمون ارور میده. پس چی کار کنیم؟ هیچی. این فایل :
config/routes.rb
رو باز میکنیم و به این شکل درش میاریم:
Rails.application.routes.draw do
namespace :api do
namespace :v1 do
resources :post
end
end
# For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
end
و حالا دوباره تست میکنیم. چطوری؟ به این شکل:
curl -X POST -H 'Content-Type: application/json' -i http://localhost:3000/api/v1/post --data '{
"title":"Hello, World",
"slug":"hello-world-abcd1234",
"body":"Hello, world! this is a simple test for my app"
}'
و خروجیای که میده به این شکله:
{
"status": "success",
"content": {
"id": 1,
"title": "Hello, World",
"slug": "hello-world-abcd1234",
"body": "Hello, world! this is a simple test for my app",
"created_at": "2021-03-28T17:44:16.608Z",
"updated_at": "2021-03-28T17:44:16.608Z"
}
}
حالا وقتشه که بریم سراغ R یعنی Retrieve/Restore. یعنی نمایش پستها و پست به صورت تکی. کافیه index و show رو یکم دستخوش تغییر کنیم:
def index
@posts = Post.all
render json: @posts
end
def show
@post = Post.find(params[:id])
render json: @post
end
و الان با ارسال چنین دستوری:
curl -X GET -i http://localhost:3000/api/v1/post/1
میتونیم پستهامون رو ببینیم.
حالا بیایم برای ویرایش هم چارهای بیاندیشیم 🙂 پس متد آپدیت هم به این شکل بهروز میکنیم:
def update
@post = Post.find(params[:id])
if @post
@post.update(post_params)
end
end
خب، اینجا چرا else نگذاشتم؟ چون اگر پست موجود نباشه (موجودیتش با if چک میشه) خود ریلز به ما ارور مناسب (معمولا ۴۰۴) رو نشون میده.
من در سیستم خودم، یک پست با آیدی ۲ دارم که قرار بود آپدیت بشه. الان اون رو با این دستور آپدیت میکنم:
خب، در نهایت فایل post_controller.rb ما به این شکل در اومده:
class Api::V1::PostController < ApplicationController
def index
@posts = Post.all
render json: @posts
end
def show
@post = Post.find(params[:id])
render json: @post
end
def create
@post = Post.new(post_params)
if @post.save
render json: {:status => "success", :content => @post}
else
render error: {:status => "fail"}
end
end
def update
@post = Post.find(params[:id])
if @post
@post.update(post_params)
end
end
def destroy
@post = Post.find(params[:id])
if @post
@post.destroy
render json: {:status => "success", :post_id => @post.id }
end
end
private
def post_params
params.require(:post).permit(:title, :slug, :body)
end
end
کل چیزی که در این قسمت نوشتم، برای درک عملکرد API کافیه. از شما هم میخوام که همین سناریو رو برای خودتون، یک دور به صورت کاملا دستی پیاده کنید و سعی کنید درش خلاقیت به خرج بدید. تجربیاتتون هم در بخش نظرات با من به اشتراک بذارید 🙂
قسمت بعدی، چی یاد میگیریم؟
در قسمت بعدی، کاری میکنیم کارستان. یاد میگیریم که در مدلها روابطشون رو تعیین کنیم. بعد مدلی میسازیم که به این مدل وابستهست. در نهایت هم به همین شکل، کنترلرش رو میسازیم و پروژه رو تحویل کارفرمای عصبانی میدیم 🙂
از این که وقت گذاشتید و این پست رو خوندید، واقعا ممنونم. هر یک ویویی که این پستها میخوره، برای من شدیدا ارزشمنده.
در پست قبلی، بررسی کردیم که اصلا MVC چیه و به چه کار میاد و یکم هم مثال ازش در روبی آن ریلز دیدیم. در این پست، قصد دارم مزایا و معایبش رو بگم و بگم که روبی آن ریلز، چه راهحلی برای مشکلاتش پیشنهاد میکنه.
باز هم لازمه که سلب ادعا کنم که این مطلب مطلقا آموزش روبی آن ریلز نیست و شما اگر میخواید روبی آن ریلز یاد بگیرید، میتونید تا پست بعدی منتظر باشید 😁
مروری بر کلیت MVC
از پست قبلی، این دید رو داریم که MVC در واقع یک روش تبدیل کردن موجودیتهای محصول به یک سری مدل، کنترل ارتباطاتشون با هم و با دنیای بیرون با یک سری کنترلر و نمایششون به صورت ویوئه (متفاوتتر از قبلی شد، نه؟ چون صرفا بیانم رو در تعریفش عوض کردم). اما حرفی از مزایای و معایبش نزدم.
عمدهترین دلیلم برای ادامه ندادن، این بود که مطلب قبلی زمان زیادی از من گرفت و حتی مجبور به ریبوت کردن لپتاپم میانه راه شدم. خود مطلب هم شدیدا طولانی بود و مطمئن بودم از یک حدی طولانیتر میشد، دیگه کسی اون رو نمیخوند.
حالا که تعاریف رو با هم بررسی کردیم، بریم سراغ اصل مطلب 🙂
مزایا و معایب معماری MVC
مزایا
تسریع فرایند پیادهسازی : همونطوری که در پست قبلی دیدید، اکثریت قریب به اتفاق چارچوبهای توسعه که با این معماری کار میکنند، ابزارهایی برای تولید و تست موجودیتها در اختیار شما میذارن. این به خودی خود موجب تسریع فرایند پیادهسازی میشه. در مورد توسعه هم این قضیه میتونه تا حدی صادق باشه، البته توسعه رو در بخش معایب بررسی میکنیم.
تسهیل فرایند کار گروهی : در یک بیزنس، معمولا بیش از یک نیرو روی محصول نهایی – چه در پیادهسازی چه در توسعه – کار میکنه. این معماری به توسعهدهندهها کمک میکنه بهتر بتونن محصول رو درک کنن. بخصوص اگر قرار باشه هر شخص روی یه بخشی کار کنه و پیادهش کنه.
تسهیل فرایند بهروزرسانی : طبیعیه که بهروزرسانی یک محصول، کار سادهای نیست. اما یک فرض ساده رو در نظر بگیرید. مثلا من، یک فروشگاه اینترنتی دارم، قراره در نسخه جدید وبسایتم، بخشی هم افتتاح کنم که افراد بتونن لوازم کارکرده هم خرید و فروش کنن. اضافه کردن یک موجودیت جدید به نرمافزاری که MVC نوشته شده به شدت سادهتر میشه و من فقط لازمه مدل بخش «کارکرده» رو اضافه کنم و روابطش با باقی اجزا رو پیادهسازی کنم.
تسهیل فرایند کشف و رفع باگ : خب از اونجایی که هر موجودیتی، در این معماری به شکلی جدا از موجودیت دیگره، خیلی راحت میشه فهمید مشکل از کجاست. البته لازمه این رو بگم که در معایب هم قراره به این بخش اشاره کنیم و در گوشتون هم میگم که Rest API در این زمینه میتونه بهتر عمل کنه 🙂
خب ما ایرانیا یک ضربالمثل معروف داریم که میگه «گل بیعیب، خداست». پس این معماری و طبیعتا فرمورکهایی که با این معماری کار میکنند هم خالی از ایراد نیستند. در این بخش از معایب MVC میگم و البته بخشیش هم از معایبی که برای ریلز و لاراول گفته شده نقل میکنم.
در مورد بخشی که از ریلز و لاراوله، اسمی از فرمورک نمیارم چون در نظرم این معایب، معایب معماری هستند و نه فرمورک. در واقع اینطوری فرض کنیم که یک سلولی که مشکلی داشته تقسیم شده و همه سلولهای جدید، همون مشکل رو دارند 😁
معایب
سختی درک معماری : این موضوع، میتونه برای برنامهنویسهای تازهکارتر، کمی مشکلساز باشه. توصیه میکنم قبل از این که لقمه بزرگ بردارید و سراغ ریلز، جنگو یا لاراول برید حتما اول با سیناترا، فلاسک یا لومن کار کنید. به این شکل فرمورکها میتونن براتون به شدت قابل درکتر بشن.
خارج شدن شکل نرمافزار از کنترل توسعهدهنده(ها) : این مورد کمی پیچیدهست. فقط بذارید به این شکل بهتون بگم که شما مثلا یه سری واژه رو نمیدونید چطور مدیریت کنید. مثلا ممکنه اسم مدل مربوط به حساب بانکی رو Hesab, Bank یا Bank Account بذارید و نفر بعدی که روی کد شما کار میکنه، با این قضیه به مشکل بخوره. بهترین راه اینه که این موارد، قبل از پیادهسازی کشف و مستند بشن.
انعطاف پایین : طبیعیه که وقتی یک جعبهابزار کامل دم دستتون باشه، دیگه سراغ ساخت ابزار نمیرید. اکثر فرمورکهای MVC هم این مشکل رو دارند و اگر شما بخواهید بخشی هم خودتون پیاده کنید، به شدت به مشکل میخورید.
پرفرمنستایم پایین : خب از اونجایی که اکثر این فرمورکها، دارن همه چیز رو خودشون هندل میکنن، ممکنه هزینه بالایی برای اجرا داشته باشند. البته این هزینه ممکنه با توجه به سختافزارهای امروزی زیاد هم بالا حساب نشه، اما چرا وقتی میشه با کمترین هزینه کاری کرد، هزینه رو بیخود و بیجهت ببریم بالا؟
رفع اشکال پرهزینه : درسته که فرایند کشف و رفع باگ رو گفتیم که آسونه، اما هزینه بالایی داره. حواسمون باید به این موضوع باشه که داریم با چیزی کار میکنیم که همه چیش به همه چیش ربط داره و قطع این ارتباط میتونه هزینه زیادی وارد کنه بهمون.
بزرگ شدن غیرقابل کنترل پروژه : خب وقتی که MVC کار میکنیم، عموما خیلی کم پیش میاد که بخواهیم موجودیتها رو به صورت سرویسهای مجزا توسعه بدیم. در واقع اینجا خبری از میکروسرویس و اینا نیست! همین باعث میشه بیزنس از یک حدی که بزرگتر بشه، کنترلش به شدت سخت شه. از همین رو، معمولا در کنار MVC یک معماری دیگری مثل rest هم استفاده میشه که این داستانها رو نداشته باشیم.
حالا بیاید ببینیم چه راهحلایی برای این قضیه داریم؟ یک راه خیلی خوب اینه که وقتی مدلی ساخته میشه، کنترلر و ویوهای مربوطه هم اتوماتیک تولید بشن. بعدها میتونیم اون چیزی که نیاز نداریم رو از این چرخه حذف کنیم. خب چطوری؟ در این بخش، دو راه حل میارم. یکی از ریلز و دیگری لاراول (فکر کنم کسی انتظار لاراول رو در این قسمت نداشت).
راه حل Ruby on Rails
در ریلز یک مفهومی داریم به اسم scaffold. این مفهوم چی کار میکنه؟ میاد مدل، ویو و کنترلر یه موجودیت رو برای ما میسازه. برای زمانهایی که Rest API نداریم و میخواهیم صفر تا صد اپ رو خودمون بزنیم، یکی از بهترین انتخابها در ساخت و طراحی موجودیتها همینه.
مثال پست بلاگ رو در نظر بگیرید. چطوری میتونیم به سادگی همهچیش رو هندل کنیم؟! سادهست، کافیه این دستور رو تایپ کنیم:
rails generate scaffold Post title:string slug:string body:text
و این دستور به خودی خودش میاد و همه چیزهایی که نیاز داشتیم رو میسازه. خوبیهای این روش چیه؟
استاندارد بودن متدها در کنترلر (یعنی نیاز نیست دیگه زحمت مضاعف برای Routing بکشیم)
وجود viewهایی که پارامترهای درست ارسال و یا دریافت میکنن (کاهش هزینه کشف و رفع باگ در مراحل اولیه توسعه)
اما خب یک بدی بزرگ هم داره و اون اینه که خلاقیت شما رو میتونه کلا نابود کنه. برای اون هم راهکارهایی هست. مثل این که شما یه کنترلر مجزا بسازید و متدها و endpointهای مورد نظر خودتون رو اونجا تعریف کنید.
باقی موارد، همه مثل مطلب قبلیه و خب در مطالبی که در آینده نزدیک خواهم نوشت، حتما اشاره خواهم کرد به این موضوع که چه فعل و انفعالاتی در ریلز رخ میده برای ساخت این ماجراها 🙂
راهحل در لاراول
یک سلب ادعای کوچک ابتدای این بخش بکنم؟ عمده تجربه من با لاراول روی Rest API بوده و فرصتی نشده که MVC باهاش کار کنم. شاید هم هیچوقت MVC کار نکنم. اما این راهحل قطع به یقین برای MVC هم پاسخگوئه.
اول از همه یک مدل میسازیم:
php artisan make:model Post
و خب وارد جزییات نمیشم، میتونیم جزییات مدل رو در فایلهای مربوطه بررسی کنیم.
بعد برای این مدل به این شکل میتونیم کنترلر بسازیم:
البته لازم به ذکره که باید دقت داشته باشید همیشه کنترلرها از جنس resource و CRUD نیستن و این دو راهکار، در واقع برای وقتی خوبن که شما نیازمند CRUD باشید و نخواهید هزینه ساخت کنترلر «از بیخ» رو متحمل بشید.
جمعبندی مطالب
در دو مطلب، هم تعاریف مرتبط با MVC رو یاد گرفتیم، هم یه نیمچه اپ MVC نوشتیم و هم مزایا و معایبش و راهحلهای احتمالی برای معایب قضیه رو بررسی کردیم.
این مطالب، بیش از این که رنگ و بوی برنامهنویسی داشته باشند، مرتبط با مهندسی نرمافزار بودند و خوشحالم که در این زمینه هم محتوایی تولید کردم. فکر کنم این پست دیگه جمعبندی خاصی نیاز نداشته باشه. فقط خواستم بابت وقتی که برای خوندن این مطلب گذاشتید، ازتون تشکر کنم 🙂
احتمالا اگر با فرمورکهایی مثل جنگو، ریلز یا ASP کار کرده باشید، عبارت MVC رو به کرات مشاهده کردید. در این پست، قصد دارم تا در مورد این معماری توضیحاتی ارائه کنم. در هر بخش هم، یک مثالی از فرمورک روبی آن ریلز آوردم.
قبل از شروع بعنوان سلب ادعا عرض کنم که شما نیاز دارید که خودتون، روبی آن ریلز رو جداگانه یاد بگیرید و این مطلب، مطلقا آموزش روبی آن ریلز نیست.
معماری MVC یعنی چی؟
خب MVC یک مخففه برای Model, View و Controller. در این معماری، یک نرمافزار (یا بهتره بگیم ایده/بیزنس) رو به سه موجودیت مدل، ویو و کنترلر تقسیم میکنیم و هر قسمت ایده یا بیزنس رو به یکی از اینها تبدیل میکنیم. در واقع، فرمورکها اینجان که ما فرایند ساخت این قضایا رو سریعتر پیش ببریم.
پس، بهتره جای این که صرفا دنبال زیربغل مار بگردیم، هر موجودیت رو جدا تعریف کنیم و ببینیم تهش به چی میرسیم!
مدل – Model
مدل، همونطور که از اسمش پیداست؛ یک مفهوم انتزاعی از یک موجودیته. بذارید یک مثال ببینیم. بیاید یک پست وبلاگ رو بررسی کنیم.
پست وبلاگ چیا داره؟ پست وبلاگ در نظر من اینها رو حتما داره:
عنوان که به عنوان یک رشته متنی در نظر گرفته میشه.
عنوان کوتاه یا slug که این هم یک رشته متنی در نظر گرفته میشه.
متن بدنه یا body که «متن» در نظر گرفته میشه.
توجه کنید که text از نظر اکثر فرمورکها با string متفاوته. این تفاوت رو احتمالا در مستندات فرمورکی که باهاش کار میکنید میتونید پیدا کنید.
حالا این مدل رو چطوری میسازیم؟ در ریلز به این صورت میتونیم یک مدل بسازیم:
rails generate model Post title:string slug:string body:text
بعد از اون هم به این شکل مایگرشنها رو اجرا میکنیم:
rails db:migrate
خب الان چی داریم؟!
الان چیزی نداریم جز یک مدل، که صرفا میگه «تعریف من از پست چیه». این تعریف کجا استفاده میشه؟ در دیتابیس. خب پس یعنی چی؟ بیاید یک تعریف کلی از مدل ارائه کنیم:
مدل عبارت است از تعاریف انتزاعی و ویژگیهاشون که در یک پایگاه داده ذخیره میشه.
خب، الان که میدونیم مدل چیه، چطور میتونیم یک نمونه ازش اجرا کنیم؟ برای نمونه در ریلز، ما اول کنسول ریلز رو باز میکنیم:
rails c
و سپس این دستور رو در کنسول وارد میکنیم:
Post.create(:title => "Hello, World", :slug => "hello-world", :body
=> "This is a first post made the wrongest way possible")
و به این شکل، ما یک پست ساختیم. اما آیا روش بهتری هم هست؟ بله هست!
کنترلرها – Controllers
طبیعتا وقتی به شما یک API داده میشه، از شما نمیخوان که با دستورات روبی پست بسازید، بهروز کنید یا حذف کنید! چیزی که به شما میدن چنین چیزیه:
GET /api/v1/posts
و این باید لیست همه پستها رو برگردونه. پس چطور این اتفاق میافته؟ با موجودیتی به اسم «کنترلر». کنترلر کارش چیه؟ کنترلر همونطور که از اسمش پیداست، میتونه کنترل کنه (یا بعبارتی چطور چشمبسته غیب بگیم؟). کنترلر وظیفهش انجام عملیات CRUD روی مدله.
حالا CRUD چیه؟ این هم یه مفهومی در بطن MVC و Rest API عه که باید بدونید چی به چیه و چی کار میکنه. عبارت CRUD مخفف Create, Restore, Update و Destroy عه. بیاید با هم دقیق بررسی کنیم:
حرف C یا Create : یعنی کنترلر باید بتونه برای من، یک نمونه از مدلم رو بسازه.
حرف R یا Restore (که در بعضی منابع Retrieve هم گفتن) : یعنی کنترلر باید بتونه محتوای یک مدل یا همه مدلها رو به من نشان بده.
حرف U یا Update : یعنی کنترلر باید بتونه در وضعیت یک نمونه از مدل، تغییری ایجاد کنه.
حرف D یا Destroy : یعنی کنترلر باید بتونه یک نمونه از مدل رو حذف کنه.
شما همون پست بلاگ رو در نظر بگیرید. این پست ممکنه نیاز داشته باشه که ویرایش بشه، نیازمند اینه که گاهی حتی حذف کامل بشه و … . این کار رو با کنترلر انجام میدیم. حالا چطوری یک کنترلر میسازیم؟ به این شکل:
rails generate controller api/v1/posts
سپس درون فایل کنترلر، این دو تابع رو برای حرف R اضافه میکنیم:
def index
@posts = Post.all
render json: @posts
end
def show
@post = Post.find(params[:id])
render json: @post
end
در یک تابع، قراره که کل پستها رو ببینیم و در یکی، فقط اونی که با ID مورد نظر ما درخواست شده نمایش داده میشه. خب، الان کافیه با ابزاری مثل curl تست کنیم API مون رو.
~/playground/mvc-tutorial/ [master] curl http://localhost:3000/api/v1/posts/1
{"id":1,"title":"Hello, World","slug":"hello-world","body":"This is a first post made the wrongest way possible","created_at":"2021-03-23T16:19:41.950Z","updated_at":"2021-03-23T16:19:41.950Z"}
حالا جهت این که C هم ببینیم، اون هم به کنترلر مربوطه اضافه میکنیم، اما بقیهش رو از مستندات ریلز میتونید یاد بگیرید 😁
پس این تابع رو به فایل کنترلرمون اضافه میکنیم:
def create
@post = Post.new(post_params)
if @post.save
render json: {:post => @post, :status => success}
else
render error: {:error => "Failed"}
end
end
این تابع چی کار میکنه؟ اول میاد از طریق یک تابع خصوصی به اسم post_params تعدادی از پارامترها مثل عنوان، عنوان کوتاه و متن بدنه رو دریافت میکنه، یک نمونه از مدل پست میسازه. اگر ذخیرهسازی پست با موفقیت انجام شه، محتوای پست رو به ما نمایش میده.
با ابزار curl به این شکل میتونیم از این بخش کنترلر استفاده کنیم:
curl -X POST -H 'Content-Type: application/json' -i http://localhost:3000/api/v1/posts --data '{
"title":"A post from a request",
"slug":"a-post-from-a-request",
"body":"This post has been create using an API call, to show you how awesome MVC can get!"
}'
البته حواستون باشه در این مورد، حتما باید هدر Content-Type رو ارسال کنیم.
این هم نمونه جوابی که این تابع باید به ما بده:
{
"post": {
"id": 2,
"title": "A post from a request",
"slug": "a-post-from-a-request",
"body": "This post has been create using an API call, to show you how awesome MVC can get!",
"created_at": "2021-03-23T17:37:55.502Z",
"updated_at": "2021-03-23T17:37:55.502Z"
},
"status": "success"
}
اما خب، چطوری این داده رو میتونیم همینجا ببینیم؟! بدون این که از curl و … استفاده کنیم؟
ویوها – View
در بخش پیش، کنترلرها رو با دیدی که برای API و … داریم ساختیم. برای استفاده از ویو، یک بار دیگه یه کنترلر میسازیم:
rails generate controller posts
در این کنترلر این دو تابع رو قرار میدیم:
def index
@posts = Post.all
end
def show
@post = Post.find(params[:id])
end
خب قرار نبود این قسمت اینجاش با کد زدن شروع شه اما خب نیاز بود درک کنیم که ویو دقیقا چیه. خب، بیایم با دو تعریف آشنا شیم.
قابل خواندن برای ماشین (Machine Readable) : این مفهوم، به معنای دادههاییه که صرفا برای کامپیوتر قابل درک و خواندن هستن و برای کاربر، خواندنشون مشکله. مثل خروجی JSON یا چیزایی که تو دیتابیس ذخیره میکنیم. وقتی از روی API دادهای میفرستیم، در واقع دادهها رو به صورت قابل خواندن برای ماشین میفرستیم و دریافت میکنیم.
قابل خواندن برای انسان (Human Readable) : این مفهوم، به معنای دادههاییه که برای انسان قابل خوندن هستند. مثل همین پستی که شما دارید میخونید. در واقع این پست در یک دیتابیس MySQL به شکل عجیب و غریب (برای انسان) ذخیره شده و وقتی شما روی لینکش کلیک میکنید، به شکل قابل خواندن برای انسان درمیاد.
اگر صرفا API داشته باشیم، کار ما MVC نیست، اما میتونیم ویو رو با استفاده از Front End داشته باشیم (که در آینده در مورد اون هم یک مطلب خواهم نوشت). اما میتونیم بیایم و برای حداقل همون حرف R یک ویو بسازیم (در حقیقت دو تا). چرا؟ چون بعضی وقتا ممکنه برنامهنویس فرانت نداشته باشیم، بعضی وقتا ممکنه حتی نیازی به فرانت نباشه و … .
برای ساخت ویو در ریلز، نیازی به اجرای دستوری نیست. در این فرمورک، وقتی کنترلر بسازید در پوشه:
app/views
یک پوشه به اسم کنترلر مربوطه ساخته میشه. پس در پوشه:
app/views/posts
دو فایل به اسمهای index.html.erb و show.html.erb ایجاد میکنیم.
در فایل index.html.erb این موارد رو باید داشته باشیم:
<h1> Posts </h1>
<% for post in @posts %>
<ul>
<li>
<a href="/posts/<%= post.id %>"> <%= post.title %></a>
</li>
</ul>
<% end %>
همونطور که میبینید، این کد دو تفاوت اساسی با HTML معمولی داره :
پسوندش یه ERB اضافه داره
کد روبی وسطش زده شده.
خب، این زبون، HTML نیست (بله، HTML هم زبانه، فقط «زبان برنامهنویسی» نیست!) بلکه زبان تمپلیتینگ خود روبیه. مثل blade در php یا Jinja در پایتون.
حالا اگر با مرورگر به localhost:3000/posts بریم این صحنه زشت رو میبینیم 😁
و اگر روی لینکها کلیک کنیم، وارد پستها میشیم:
البته یادتون باشه slug معمولا برای url کاربرد داره ولی چون اینجا قصد آموزش ریلز نبود، از این ماجرا صرف نظر کردیم.
خب، حالا بعد یک مطلب طولانی، میدونیم که چی به چیه؛ بیاید با هم جمعبندی کنیم.
جمعبندی مطلب
اول از همه در این مطلب به یک تعریفی برای MVC رسیدیم. در واقع، کار ما بعنوان یک مهندس نرمافزار، تبدیل مفاهیم و موجودیتهای یک بیزنس و روابطشون با هم به یک سری مدل، کنترلر و نحوه نمایششون به کاربر در فرم ویو خواهد بود. حالا که یک تعریفی از MVC داریم، به نظرم بد نیست که بیایم هر مورد هم یک بار دیگر، بررسی کنیم.
مدل : مدلها، تعاریف انتزاعی از موجودیتها، ویژگیها و روابط بینشون هستند. به عبارتی، شکلی که ما قراره داده خام رو پردازش کنیم، در مدل تعیین میشه. کمی راحتترش کنم؟ مدل در واقع ساختار دیتابیس ماست.
کنترلر : کنترلر وظیفه ساخت یک نمونه از مدل، استخراج داده از دیتابیس، بروزرسانی داده در دیتابیس و حذف یک رکورد از دیتابیس رو عهده داره. این هم اگر بخوام راحت توصیف کنم، میگم ابزاری که باهاش دیتابیس رو به شکل سادهتری کنترل میکنیم.
ویو : ویو، نتیجه یک کوئری رو به صورت قابل خواندن توسط انسان، به ما نمایش میده. نتیجه کوئری معمولا به صورت یک جدول در دیتابیس (در واقع یک یا چند سطرش) یا به صورت JSON برمیگرده که توسط انسان قابل خوندن نیست. ما در ویو (و البته اغلب فرانت) این اتفاق رو رقم میزنیم.
البته موارد دیگری هم هستند که دیگه واقعا از حوصله این مطلب خارج شدند. دلیل عمدهش؟ طولانی شدن بیش از حد این مطلب. وگرنه دوست داشتم از معایب و مزایای MVC هم حرف بزنم. یک اتفاق بامزه هم این وسط افتاد که من حتی مجبور شدم لپتاپ رو ریبوت کنم و شانس آوردم که دو سه ساعت فکر و نوشتن، نپریده بود 😂
در پایان، از همه شمایی که این مطلب رو خوندید تشکر میکنم که وقت گذاشتید بابت خوندن مطلب. ضمنا، این اتفاقات در فرمورکی که شما استفاده میکنید چطوری رقم میخوره؟ در نظرات منتظرتونم 🙂
مدتی میشه که شروع کردم به یادگیری زبان جاوا چرا که این زبان، علیرغم این که زبان قشنگ و پرکاربردیه، کمک میکنه برنامهنویس بهتری هم بشم. گذشته از اون، مفاهیم شیءگرایی در جاوا به خوبی پیاده شدند و میشه تا حد زیادی هم برنامهنویسی و طراحی شیگرا رو با این زبان یاد گرفت.
خلاصه این که در ساعات بیکاری، دموی رایگان آموزش جاوای مشفق همدانی عزیز (وبسایت) رو دیدم و کمی هم در مورد جاوا و فرمورکهاش و …، تحقیق کردم. خلاصه که خواستم یه پروژه فان بزنم و نتیجتا تصمیم گرفتم کمی پروژه رو چالشی کنم. به همین خاطر تصمیم گرفتم که از دیتابیس در پروژه استفاده کنم. رسیدن به این نتیجه که الان در قالب آموزش برای شما درآمده، کار سادهای نبود ولی خب ارزشش رو داشت.
اسکولایت (SQLite) چیه؟
اسکولایت یک سیستم مدیریت پایگاهداده رابطهایه که در سال ۲۰۰۰ توسط ریچارد هیپ ساخته شده. این RDBMS عزیز، برخلاف نمونههای بزرگ و معروفی مثل MySQL, PostgresSQL, SQL Server و … که نیازمند یک سرور جداگانه هستند، نیازی به داشتن سرور نداره و همه چیز رو در یک فایل، عموما با پسوند sqlite یا sqlite3 ذخیره میکنه و اطلاعاتی مثل روابط و دادهها رو روی اون فایل مینویسه.
این نوع مدیریت دیتابیس برای نرمافزارهایی که نیاز دارند داده رو به صورت محلی ذخیره کنند و همچنین سرویسهای آنلاین کوچک که کاربر زیادی ندارند کاربردیه. همچنین میتونه برای ذخیره موقت اطلاعات هم استفاده بشه. به همین خاطر، محبوبیت زیادی کسب کرده. گذشته از این، در بسیاری از چارچوبها مانند جنگو یا ریلز، در محیط آزمایش (test) و توسعه (development) معموله که از SQLite استفاده کنند تا صرفا ببینن که آیا ساختار دادههاشون درسته یا خیر.
تعریف پروژه
خب، حالا که میدونیم قراره از جاوا و SQLite استفاده کنیم، بیایم پروژه رو تعریف کنیم. پروژه برای من به این شکل تعریف شده بود:
برنامهای بنویسید که یک نام و یک شماره تلفن از کاربر دریافت کرده و سپس آن را در یک پایگاهداده SQLite ذخیره کند.
برای این که پروژه رو کمی متفاوت از چیزی که خودم برای تمرین زدم کنیم، صورتش رو به این شکل تغییر میدیم :
برنامهای بنویسید که یک نام، شماره تلفن و ایمیل از کاربر دریافت کرده و سپس آن را در یک پایگاهداده SQLite ذخیره کند.
ضمنا، من از سیستمعامل لینوکس (در حال حاضر اوبونتو/دبیان) استفاده میکنم و چیزایی که در این آموزش بهشون اشاره میکنم عموما دستورات لینوکسی هستند. اگر ویندوزی هستید هم میتونید از WSL استفاده کنید یا دستورات مشابه پیدا کنید.
آمادهسازی و ایجاد پروژه
پس از باز کردن IntelliJ IDEA و سپس ایجاد یک پروژه Command Line جدید، از منوی File و زیرمنوی Project Structure زبانه Dependencies را انتخاب کرده و سپس فایل jar ای که برای SQLite JDBC Driver دریافت کردهاید را به پروژه وارد کنید. همچنین، من با دستور زیر، یک دیتابیس خالی در پوشهای در کامپیوتر خودم ایجاد کردم:
touch /home/prp-e/playground/java.sqlite3
به این شکل، من یک فایل قابل فهم برای SQLite ساختهام. قدم بعدی، آشنایی نسبی با پرسوجوهای SQLite است.
کمی آشنایی با SQLite
مثل هر دیتابیس SQL دیگری، SQLite هم مجموعهای از پرسوجوها اجرا میکنه تا به نتیجهای که ما میخوایم برسه. در این بخش، اونهایی که در پروژه استفاده میشن رو بررسی میکنیم.
ساخت جدول جدید
همونطور که اول گفتیم، قراره در جدول تماسهامون، نام، تلفن و ایمیل اشخاص رو ذخیره کنیم. به همین خاطر موارد رو لحاظ میکنیم:
یک شناسه منحصر به فرد برای هرفرد که به صورت خودکار، افزایش پیدا کنه
اسم فرد به صورت یک رشته متنی
شماره فرد به صورت رشته متنی (برای شماره هایی که – یا + و … دارند)
ایمیل فرد به صورت رشته متنی
پس پرسوجوی اول ما به این شکل میشه :
CREATE TABLE IF NOT EXISTS contacts(id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, number TEXT NOT NULL, email TEXT NOT NULL);
در این پرسوجو، داریم میگیم که اگر جدولی به اسم contacts نبود بسازش. برای هر کاربر یک شناسه به صورت PRIMARY_KEY یا همون شناسه اولیه/منحصر به فرد درنظر بگیر که به صورت خودکار افزایش پیدا کنه. اسم، شماره و ایمیل هم به صورت متن دریافت کن اما خالی نباشن.
اضافه کردن تماس جدید
برای اضافه کردن تماس جدید، کل کاری که باید انجام بدیم اجرای این پرس و جوئه :
در این پرس و جو میگیم که یک تماس جدید با مشخصات شخصی به نام John Doe به این پایگاه داده اضافه بشه.
پیادهسازی در جاوا
برای پیادهسازی پروژه، ما نیاز به این موارد داریم:
تابعی که اسم دیتابیس، پرسوجوی اولیه و پرسوجوی مورد نظر ما رو دریافت کنه
نوشتن برنامهای که کار دریافت اطلاعات رو انجام بده
تابع دریافت اطلاعات
public static void database_init(String db_address, String init_query, String query){
String conn = "jdbc:sqlite:/" + db_address;
try {
Connection connection = DriverManager.getConnection(conn);
System.out.println("Connection established to the database.");
Statement statement = connection.createStatement();
statement.execute(init_query);
System.out.println("Successfully created the table.");
statement.execute(query);
System.out.println("Successfully ran the query");
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
همونطوری که میبینید، این تابع آدرس دیتابیس ما رو میگیره، اون رو به JDBC میده. پس از اون، یک اتصال با دیتابیس ایجاد میکنه و اگر اتصال موفقیتآمیز بود، یک پیام به شما میده. سپس، یک statement از دیتابیس ایجاد میکنه و پرس و جوی اولیه رو اجرا میکنه که در اینجا ساخت جدوله. پس از اون هم، کوئری نهایی یا اصل کاری اجرا میشه که در اینجا، میشه همون اضافه کردن تماس به جدول تماسها. همچنین تعریف شده که پس از اجرای هر پرسوجو، یک پیام مناسب چاپ بشه.
در مورد Connection, Statement و try catch ها نگران نباشید. اینها چیزایین که توسط IntelliJ IDEA هندل میشن. فقط موقع ساخت Connection حواستون باشه که بهتون خطا میده و ازتون میپرسه که «آیا در try catch قرارش بدم؟» و شما باید تاییدش کنید.
حالا که تابع ارتباط با دیتابیس رو داریم، وقتشه که اطلاعات رو از کاربر بگیریم. این کار رو من در تابع main میکنم.
تابع اصلی
در تابع اصلی، لازمه که نام، تلفن و ایمیل افراد رو با پیام مناسب، دریافت کنیم. این تابع اصلی برنامه من شد:
public static void main(String[] args) {
// write your code here
String query = "CREATE TABLE IF NOT EXISTS contacts(id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, number TEXT NOT NULL, email TEXT NOT NULL);";
Scanner name = new Scanner(System.in);
Scanner number = new Scanner(System.in);
Scanner email = new Scanner(System.in);
System.out.print("Enter a name: ");
String input_name = name.nextLine();
System.out.print("Enter a phone number: ");
String input_number = number.nextLine();
System.out.print("Enter an email address:");
String input_email = email.nextLine();
String insertion_query = "INSERT INTO contacts (name, number, email) VALUES (\"" + input_name + "\", \"" + input_number + "\"" + ", \"" + input_email +"\")";
database_init("/home/prp-e/playground/java.sqlite3", query, insertion_query);
}
همونطور که میبینید ابتدا یک رشته به اسم query دارم که در اون توضیح دادم دقیقا میخوام چه جدولی با چه مشخصاتی برام ساخته شه. همچنین چک بشه که اگر جدول موجوده، ساخته نشه و روی همون جدول موجود دادههای جدید وارد بشن.
بعد از اون، سه تا شیء Scanner ایجاد کردم که ورودی رو از ترمینال کاربر بخونن. پس از اون، پیامهای مناسب رو چاپ کردم و مقداری که Scanner از شما دریافت کرده رو داخل رشتههای جدید ریختم.
پس از اون هم یه پرسوجوی بزرگ ساختم و در اون تعریف کردم که چیزایی که توسط Scanner ها خونده شدند، دقیقا کجا قرار بگیرند. در نهایت، هرچه بود و نبود رو دادم به تابع database_init .
برنامه کامل
این هم کد برنامه کامل که ببینید چی به چیه:
package com.haghiri75;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Scanner;
public class Main {
public static void database_init(String db_address, String init_query, String query){
String conn = "jdbc:sqlite:/" + db_address;
try {
Connection connection = DriverManager.getConnection(conn);
System.out.println("Connection established to the database.");
Statement statement = connection.createStatement();
statement.execute(init_query);
System.out.println("Successfully created the table.");
statement.execute(query);
System.out.println("Successfully ran the query");
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
public static void main(String[] args) {
// write your code here
String query = "CREATE TABLE IF NOT EXISTS contacts(id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, number TEXT NOT NULL, email TEXT NOT NULL);";
Scanner name = new Scanner(System.in);
Scanner number = new Scanner(System.in);
Scanner email = new Scanner(System.in);
System.out.print("Enter a name: ");
String input_name = name.nextLine();
System.out.print("Enter a phone number: ");
String input_number = number.nextLine();
System.out.print("Enter an email address:");
String input_email = email.nextLine();
String insertion_query = "INSERT INTO contacts (name, number, email) VALUES (\"" + input_name + "\", \"" + input_number + "\"" + ", \"" + input_email +"\")";
database_init("/home/prp-e/playground/java.sqlite3", query, insertion_query);
}
}
جمعبندی
گرچه این پروژه خیلی از مفاهیم شیگرایی رو در خود نداره، تا حد زیادی هم به نظر خودم کثیف نوشته شده و میشد از این بهتر باشه، اما پروژه خوبی برای سرگرم شدن به مدت ۲-۳ روز باشه. همچنین توضیح دوبارهش باعث شد که کمی کد رو تمیزتر کنم و سپس مطلب رو بنویسم.
این پروژه، برای به چالش کشیدن خیلی از تواناییهای شما، میتونه گزینه خوبی به حساب بیاد. فلذا اگر میخواید کمی توانایی برنامهنویسی خودتون رو محک بزنید، توصیه میکنم که شما هم این پروژه رو پیادهسازی کنید 🙂
مدتی پیش، خواستم یک نمونه شخصی از ماستودون بسازم. موقع نصب، ارورهای عجیب و غریب دریافت کردم و خب شروع کردم به جست و جو پیرامون این که چرا اینطوریه و خیلیها واضحا بیان کردند که «مشکل از تصویر داکر PostgresSQL بوده و باید روی اون تصویر کمی تغییر داده شه».
حقیقتا من از PGSQL استفاده نکردم و خیلی هم دلم نمیخواست وارد قضیه بشم و روی داکرفایل و داکر کامپوز و …، تغییر ایجاد کنم. به نظرم همون چیزی که خود توسعهدهنده پیشنهاد میکنه، معمولا بهترین حالتیه که داره با نرمافزار کار میکنه. اما یه ایدهای به سرم زد و اون هم این بود که چی میشه اگر پشتیبانی از MySQL رو به این بزرگوار اضافه کنم؟ نتیجه این شد که تصمیم گرفتم این مطلب رو بنویسم 😀
ماستودون چیه؟
ماستودون یک شبکه اجتماعی از نوع «فدریتد» (انگلیسی : Federated) محسوب میشه. به این شکل که وقتی شما روی سرور خودتون هم اون رو اجرا کنید، میتونید باقی سرورها رو ببینید و تعامل کنید.
مدتی میشه که خیلی از دوستانم، ماستودون رو بعنوان شبکه اجتماعی اصلی استفاده میکنند و خیلی کمتر به توییتر میان. شاید هم کلا توییتر نیان. یکی از دلایلش همینه که شما، صاحب دادههای خودت هستی. این یک ایده خیلی خوبه که اگر واقعا حریم خصوصی و گردش اطلاعات براتون مهمه، به سمت ماستودون مهاجرت کنید.
نگاه به کد و چندکلمهای صحبت پیرامون اون
خب از اینجا اصل ماجرا شروع میشه. کد ماستودون با فرمورک روبی آن ریلز (لینک) نوشته شده. این برای من خیلی خوشحالکننده بود چون خوندن و تغییر دادن اون کد رو آسون میکرد. اما این هم باید در نظر گرفت که کد، به شدت بزرگه و حتی برای حرفهایهای روبی آن ریلز هم خوندنش کمی دشواره. بهرحال میشه یکم حوصله به خرج داد و کد بزرگ رو درک کرد. این مشکل کد نیست. بلکه مشکل اون ساختار MVC محسوب میشه که بعد یه مدتی کد رو به شدت بزرگ میکنه و پیمایش و رفع اشکالش رو سخت.
در نگاه اول، کد واقعا ایراد خاصی (جز ایرادات مرسوم پروژههای مشابه) رو نداره. اما اولین ایراد جایی خودش رو نشون میده که شما چندین کتابخونه سیستمی اضافی نیاز دارید. برای من libicu و libidn و اینا بود. یکم جا خوردم که چرا باید اینها نصب باشه؟ اما با کمی تحقیق در موردش، فهمیدم که یک سری از کتابخونههای روبی هستند که وابسته به این بزرگوارهان. در واقع، هردوی اینها به شکلی برای پردازش متن استفاده شدند و خب این که پردازش متن، از طرف سیستم انجام بشه خیلی منطقیه. این موضوع فقط کمی باعث بزرگتر شدن پروژه شده. اما خوبیهاش، به شدت بیشتر از بدیهاشه.
مورد بعدی اروری بود که سر PGSQL گرفتم. خب حقیقتا انتظار این یکی رو داشتم. چون PGSQL و کتابخونههای سیستمیش، روی سیستم من نصب نیستند (البته شاید بعدها مجبور شم برای اشکال زدایی و کمک به همین ماستودون، نصبش کنم) و خب اروری که گرفتم دقیقا همین رو میگفت. وقتی مطمئن شدم که هیچ کتابخونه دیگری مشکلی نداره، دست به کار شدم که PGSQL رو با MySQL عوض کنم.
پروسه تغییر سیستم مدیریت پایگاه داده
خب معمولش اینه که اول، به پروژه میگیم کتابخونههای لازم رو نصب کنه. نیاز بود روی سیستم libmysqlclient-dev هم نصب کنم و کردم. بعدش در Gemfile، جمهای مربوط به مدیریت MySQL رو اضافه کردم و جمهای مربوط به PGSQL رو به دیدگاه تبدیل کردم و رفتم سراغ مراحل ساخت پروژه.
متاسفانه، اینجا کلی ارور گرفتم سر یک سری جم خاص! جمهایی که مرتبط با PGSQL هستند و شدیدا Hard Code شدن رو در کد منبع، شاهد بودیم.
قسمتیش رو دستی حذف کردم به امید این که کار کنه. اما موقع اجرای Migration ها، ارورهای جدیدی گرفتم. قسمت بزرگی از Migration ها رو چک کردم و دیدم که یه جاهایی حتی به صورت دستی دستورات PGSQL نوشته شده. حس میکردم که دیگه از اینجا جلوتر رفتن، یه جورایی مسخره کردن خودم باشه. حقیقتا بود، چون اصلا آشنا نبودم به این که اینجا چه اتفاقی داره میافته و خب بیخیال اضافه کردن پشتیبانی MySQL به این پروژه شدم.
جمعبندی
همونطوری که عرض کردم خدمت شما، بزرگترین اشکال این کد اینه که شدیدا وابسته به PGSQL عه. اگر اینقدر هاردکد نمیشد، شاید وضعیت طور دیگری بود و میشد پشتیبانی پایگاههای دیگر رو بهش اضافه کرد. متاسفانه در issue هایی که در پروژه بود هم دیدم که خیلی صریح پیشنهادات مرتبط با MySQL رو رد کردند و امیدم رو به طور کلی به این پروژه برای اضافه کردن این دیتابیس، از دست دادم.
یک توصیه نهایی بخوام به دوستان توسعهدهنده کنم اینه که : سعی کنید حداقل در زمینه دیتابیس، وابستگی رو به حداقل خودش برسونید. اول از همه با این فرض که ممکنه هرلحظه شما به هردلیلی بخواید سیستم رو عوض کنید. پس هرقدر وابستگی کمرنگتر باشه، بیشتر به نفع خودتونه.
مورد بعدی هم اینه که اگر نفر بعدی به هر دلیلی خواست با سیستم متفاوتی پروژه رو اجرا کنه، انقدر سختش نباشه. بهرحال، همه قرار نیست همه چیز رو اونطوری که من و شما دوست داریم، استفاده کنن.
مصادف با عید قربان امسال، پروژه API ترجمه فارسی قرآن کریم رو منتشر کردم. این پروژه، پروژه سختی برای من نبود اما چیزای جالبی ازش یاد گرفتم که خب بد نیست در موردش مطلبی بنویسم. اما قبل از اون، لازمه یک چیزی رو توضیح بدم …
چه شد که این پروژه رو شروع کردم؟
حقیقتش شروع این پروژه به این دلیل بود که شدیدا دنبال ابزار مشابهی میگشتم. با این که بعد از این که این پروژه رو دپلوی کردم، یک دوستی در توییتر به من پیشنهاد استفاده از این ابزار رو داد؛ اما بهرحال من ابزار رو توسعه داده بودم و دپلوی هم کرده بودم.
خلاصه که، شروع این پروژه برای من جالب بود. چرا که پروژه به خودی خود سخت نبود اما وسطاش، دقیقا جایی که داشتم فرانتند براش میساختم، چالشی شد. به همین خاطر تصمیم گرفتم که این مطلب رو بنویسم و توضیح بدم که چرا این چالش برای من پیش اومد 🙂
مفهوم تازه : CORS
خب، وقتی که ما بکند و فرانتند پروژه رو از هم جدا میکنیم، در واقع داریم دوتا origin مختلف رو به هم وصل میکنیم. طبق استانداردهای وب؛ اصولا چنین کاری در مرورگرها؛ بخاطر مسائل امنیتی مجاز دونسته نشده. به همین خاطر، ما مفهوم تازهای به اسم «چند ریشهای» یا Cross-Origin رو خواهیم داشت.
حقیقتا وقتی فرانتند رو با جاوااسکریپت مینوشتم؛ دیدم که یک سری ارور بهم میده و توی اون ارورها بهم میگه که بکند من، برای «اشتراک گذاری منابع چندریشهای» یا Cross-Origin Resource Sharing تهیه نشده. دوست داشتم از خود کد جاوااسکریپتم درستش کنم اما چون قرار بود روی هاست همین وبلاگ قرار بگیره و نه سرور مجزایی؛ ترجیح دادم که به جای node.js از وانیلا جی اس استفاده کنم (توضیح : وانیلا اصطلاحا به نرم افزار یا ابزار دست نخورده گفته میشه) و خب راهکاری براش پیدا نکردم (شاید هم چون درست دنبالش نگشتم) ولی از اونجایی که بکند رو با روبی و سیناترا نوشته بودم، براش راهکار پیدا کردم و این راهکار رو در یک ویرگول به اشتراک گذاشتم (لینک).
همین یک کار کوچک، باعث شد که من مفهوم کورز و صد البته نحوه کانفیگ درستش در بکند رو یاد بگیرم، از این جهت هم واقعا خوشحالم چرا که اکثر توسعه دهنده های فرانت، مدام از این قضیه که در بکند این مشکل حل نمیشه، نالانند ((:
چیزی که سالها عقبش انداخته بودم : زرین پال!
تقریبا از وقتی که فهمیدم اینترنت چیه، دنبال راهی برای کسب درآمد ازش بودم. فکر کنم دوم سوم دبیرستان بودم که سایت زرین پال به وجود اومد و اون موقع (چون به سن قانونی نرسیده بودم) نمیتونستم ازش استفاده کنم؛ یا این که باید از کسی میخواستم که قبول کنه به اسمش اکانت باز کنم (مثلا پدر یا مادرم).
از وقتی به سن قانونی رسیدم هم هرروز میخواستم برم و اکانت زرین پال درست کنم تا ازش برای حمایت و فروش و … روی اینترنت استفاده کنم. خلاصه که بالاخره همزمان با ریلیز این پروژه، این اتفاق هم افتاد ((: تازه دقیقتر بخواهیم حساب کنیم، برای این مساله هم مجبور شدم کارت بانکیم رو تمدید کنم و هم برای کارت هوشمند ملی اقدام کنم!
به طور خلاصه، این پروژه پروژه جالبی برام بود. هم چالشاش هم ساختارش و هم اتفاقات پیرامونش. ضمن این که به خود پروژه هم ابتدای مطلب لینک دادم و امیدوارم بخونید و خوشتون بیاد. اگر هم از پروژه قراره استفادهای کنید، حمایت هم بکنید ممنون میشم (:
وبلاگ شخصی محمدرضا حقیری، برنامهنویس، گیک و یک شخص خوشحال