آموزش کار با کلید خارجی در room برای اندروید در 15 دقیقه
![آموزش کار با کلید خارجی در room برای اندروید در 15 دقیقه](https://nouri.academy/wp-content/uploads/2023/12/foreign_key_in_room_database_android_nouri_academy.webp)
![آموزش کار با کلید خارجی در room برای اندروید در 15 دقیقه](https://nouri.academy/wp-content/uploads/2023/12/foreign_key_in_room_database_android_nouri_academy.webp)
room database کتابخانه ایست که به عنوان لایۀ انتزاعی ای میان پایگاه دادۀ SQLite و محیط برنامه نویسی اندروید مطرح می باشه. تمام امکاناتی که در SQLite می تونیم پشتیبانی کنیم، در این کتابخانه فراهم است. اگر دوست داری بدونی چطور از امکان کلید خارجی در room استفاده کنی، این مقاله رو بخون.
کلید خارجی در room برای برنامه نویسی اندروید
کسایی که با SQLite یا MySQL آشنایی داشته باشن حتما دربارۀ مفهوم کلید خارجی شنیدن، مفهوم کلید خارجی (Foreign Key)به ما کمک می کنه تا میان دو جدول ارتباط بر قرار کنیم. اندکی به بررسی شماتیک این قضیه می پردازیم و سپس بحث کلید خارجی در room رو باز تر می کنیم.
رابطۀ یک به چند میان دو جدول با استفاده از کلید خارجی در room چگونه می تواند باشد؟
جدول یادداشت و جدول پوشه با هم ارتباط یک به چند دارند و خطی که میان این دو جدول در شماتیک بالا رسم شده نشان دهنده همین قضیست(یک به چند یعنی یک رکورد از جدول پوشه می تونه با بیش از یکی از رکورد های جدول یادداشت ارتباط داشته باشه).
فیلد folder_id در جدول یادداشت کلید خارجی ماست که با کلید اصلی یا primary key در جدول پوشه، ارتباط داره. یک به چند بودن یک ارتباط یعنی، به تعداد دلخواه رکورد از جدول یادداشت داریم که می تونن با یک رکورد از جدول پوشه ارتباط داشته باشن، اما فقط یک رکورد از جدول پوشه، و نه بیشتر.
هر تعییری در نام (و یا سایر اطلاعات) رکورد مدنظر در جدول پوشه، قرار بگیره؛ بلافاصله به اطلاع تمام رکورد های یادداشت که با رکورد مورد تغییر قرار گرفته، ارتباط دارند؛ خواهد رسید.
کلید خارجی در room بهمون همچنین کمک می کنه تا اگر آن رکورد جدول پوشه، حذف شد؛ تصمیم بگیریم چه اتفاقی برای رکورد های جدول یادداشت که باهاش ارتباط داشتن بیاد؛ مثلا همشون حذف بشن یا وارد ارتباط با یک رکورد دیگه از جدول پوشه قرار بگیرن. همۀ اینها با کمک کلید خارجی در room ممکن هست.
سناریو پوشه و یادداشت به این شکله: یادداشت ها داخل پوشه ها قرار می گیرند، درست عین ویندوز با FileManager گوشی اندرویدی. پوشه ها عکس و اسم دارند؛
یادداشت ها title (عنوان یا اسم) دارند، توضیحات متنی (des) داخل خودشون به صورت نوشته دارند، درون یک پوشه ای قرار گرفته اند که id مشخصی داره و یکی از رکورد های جدول folders_table هست، تاریخ ایجاد دارند، ساعت دارند، درجۀ اهمیت (priority) دارند که در انتهای متن یادداشتی به سه صورت رو برو درج می شه:(کم، متوسط، زیاد)
همچنین یادداشت های ما ممکنه پین یا سنجاق شده باشن
این سناریوی یک اپلیکیشن هست که از کلید خارجی در room برای دو جدول folder و note خودش برای استفاده از مفهوم پوشه و جدول درون محیط خودش استفاده کرده. داخل اپلیکیشن پوشه ها و یادداشت ها وجود دارند.
یک امکان دیگه ای هم که اپلیکیشن به ما می ده اینکه می تونیم با زدن روی دکمۀ صدور در حافظۀ گوشی؛ یادداشت هامون به همراه پوشه هاشونو توی FileManager گوشی استخراج کنیم؛ این یعنی هر آنچه که پایگاه دادمون بو به کمک کلید خارجی در room بکار بردیم قابل معادل سازی به صورت پوشه ها و فایل های txt واقعی درون FileManager گوشی هست.
اگر در خصوص نحوۀ ایجاد فایل در اندروید از api 21 تا api 33 کنجکاو هستی این مقالمو بخون
پس ببینید کلید خارجی در room چقدر بدرد بخوره، چون اگر نمی بود ما چطور می خواستیم ارتباط یک به چند رو ادا کنیم و بفهمیم که کدوم یادداشت توی کدوم پوشه هست که صحت اطلاعات ما رو حفظ بکنه؟
عکس زیر رو در رابطه با پوشه ها و یادداشت ها در محیط اپلیکیشن نگاه کن:
لیست سمت چپ لیست یادداشت هاست و لیست سمت راست لیست پوشه هاست. دوتا پوشه با نام های «خرید» و «کار» داریم. یادداشت فهرست خرید داخل پوشۀ خرید هست و یادداشت کار، داخل پوشۀ کار هست.
اگر این پوشه ها رو توی FileManager صدور بکنیم سلسله مراتب پوشه و فایل های یادداشت رعایت می شه:
![کلید خارجی در room](https://nouri.academy/wp-content/uploads/2023/10/room_foreign_2.jpg)
![کلید خارجی در room](https://nouri.academy/wp-content/uploads/2023/10/room_foreign_2.jpg)
پوشه های کار و خرید
![کلید خارجی در room](https://nouri.academy/wp-content/uploads/2023/10/room_foreign_3.jpg)
![کلید خارجی در room](https://nouri.academy/wp-content/uploads/2023/10/room_foreign_3.jpg)
ذخیرۀ متحوای یکی از رکورد های جدول یادداشت به صورت فایل txt در fileManager
ما اطلاعات رو در پایگاه دادۀ room ذخیره می کنیم و می تونیم از اطلاعاتمون خروجی به صورت فایل بگیریم یا توی اپلیکیشنمون به صورتی که مشاهده کردید نمایش بدیم.
اما کلید خارجی در room کمک های دیگه ای هم به ما می کنه: مثلا فرض کنید پوشۀ «کار» رو بخوایم تغییر نام بدیم به پوشۀ «اداره»، به صورت خودکار تمام یادداشت هایی که درون پوشۀ «کار» هستند بروزرسانی می شن و اعلام می کنند که درون پوشۀ «اداره» هستند:
![کلید خارجی در room](https://nouri.academy/wp-content/uploads/2023/10/room_foreign_4.jpg)
![کلید خارجی در room](https://nouri.academy/wp-content/uploads/2023/10/room_foreign_4.jpg)
کلید خارجی در room صحت اطلاعات رو حفظ می کنه
ما می تونیم حتی icon پوشمون رو هم تغییر بدیم و مطمئن باشیم که یادداشت ها از تغییر icon پوشهشون آگاه خواهند شد.
ما یک پوشۀ پیش فرض با نام «بیپوشهها» داریم که یادداشت های بدون پوشه درون آن قرار می گیرن. می خواهیم اگر یک پوشه ای حذف شد تمام یادداشت های درون آن پوشه توی «بیپوشهها» قرار بگیرن. کلید خارجی در room اینجا به کمکمون می یاد، بهش می فهمونیم اگر پوشه ای حذف شد همۀ یادداشت های توش رو بریزه داخل «بیپوشهها».
الان پوشۀ «اداره» رو حذف می کنم:
![کلید خارجی در room](https://nouri.academy/wp-content/uploads/2023/10/room_foreign_5.jpg)
![کلید خارجی در room](https://nouri.academy/wp-content/uploads/2023/10/room_foreign_5.jpg)
حذف رکورد پوشۀ اداره از جدول folders_table، باعث می شود رکورد هایی که در جدول note_table هستند و با رکورد پوشۀ «اداره» ارتباط دارند؛ وارد ارتباط با پوشۀ «بیپوشهها» که رکورد دیگری از جدول folders_table می شن. و این سناریو با استفاده از کلید خارجی در room شدنی هست.
دربارۀ کاربرد کلید خارجی در حذف و ویرایش اطلاعات جدول پدر (پوشه) و بروز شدن اطلاعات جدول فرزند (یادداشت)، صحبت کردیم
اکنون وارد بحث کد نویسی کلید خارجی در room می شیم.
پیاده سازی کلید خارجی در room در محیط اندروید استودیو
حتما با Room Database تاحالا کار کردی که سراغ بحث کلید خارجی در room اومدی؛ پس از توضیحات اضافه دربارۀ خود room خودداری می کنم.
اول دوتا entity (جدول) تعریف می کنیم، تا با استفاده از کلید خارجی در room میانشون ارتباط برقرار بکنیم. entity اول، FolderEntity هست:
@Entity(tableName = FOLDERS_TABLE_NAME) data class FolderEntity( @PrimaryKey(autoGenerate = true) @ColumnInfo(name = FOLDER_ID) var id : Int = 0, @ColumnInfo(name = FOLDER_IMG) var img : Int = 0, @ColumnInfo(name = FOLDER_TITLE) var title : String = "" )
idمون از نوع کلید اصلی یا primary key هست و autoGenerate اش برابر با true هست؛ یعنی هر رکوردی که توی این جدول ایجاد می کنیم به صورت خودکار id اون یکی نسبت به id رکورد قبلی بیشتر می شه. شماتیک این جدول رو در عکس ابتدای مقاله آوردم.
ما از ثابت ها بجای وارد کردن مستقیم رشته ها درون ساختار برناممون بکار می بریم و همۀ آنها در یک فایل کاتلینی مجزا با نام Constants نوشته شدند:
const val FOLDERS_TABLE_NAME = "folders_table" const val NOTE_TABLE_NAME = "note_table" const val NOTE_DESK_DATABASE_NAME = "note_desk_database" //folder columns const val FOLDER_ID = "folder_id" const val FOLDER_IMG = "folder_img" const val FOLDER_TITLE = "folder_title" //note columns const val NOTE_ID = "note_id" const val NOTE_TITLE = "note_title" const val NOTE_DES = "note_des" const val NOTE_FOLDER_ID = "note_folder_id" const val NOTE_DATE = "note_date" const val NOTE_TIME = "note_time" const val NOTE_PRIORITY = "note_priority" const val NOTE_IS_PINNED = "note_is_pinned"
کلید خارجی در room برای entity که نقش فرزند رو داره (در اینجا entity یادداشت) تعریف می شه. کلید خارجی در room باید به کلید اصلی یا primary key در entity والد (در اینجا FolderEntity) پیوند بخوره. کلاس noteEntity بدین صورته:
@Entity( tableName = NOTE_TABLE_NAME, foreignKeys = [ForeignKey( entity = FolderEntity::class, parentColumns = arrayOf(FOLDER_ID), childColumns = arrayOf(NOTE_FOLDER_ID), onUpdate = ForeignKey.CASCADE, onDelete = ForeignKey.SET_DEFAULT )] ) data class NoteEntity( @PrimaryKey(autoGenerate = true) @ColumnInfo(name = NOTE_ID) var id: Int = 0, @ColumnInfo(name = NOTE_TITLE) var title: String = "", @ColumnInfo(name = NOTE_DES) var des: String = "", @ColumnInfo(name = NOTE_FOLDER_ID, defaultValue = "1", index = true)//NO_FOLDER is default var folder_id :Int = 0, @ColumnInfo(name = NOTE_DATE) var date: String = "", @ColumnInfo(name = NOTE_TIME) var time: String = "", @ColumnInfo(name = NOTE_PRIORITY) var priority: String = "", @ColumnInfo(name = NOTE_IS_PINNED) var isPinned: Boolean = false )
انوتیشن Entity یک پارامتر برای tableName داره که بویسلۀ آن، نام جدول رو تعیین می کنیم؛ پارامتر های دیگه ای هم داره، foreignKeys یکی دیگه از آن پارامتر هاست که یک لیست از کلاس ForeignKey دریافت می کنه؛ ما برای سناریومون فقط به یک کلید خارجی در room نیاز داریم، برای همین لیست ما فقط شامل یک شئ از کلاس ForeignKey می شه.
@Entity( tableName = NOTE_TABLE_NAME, foreignKeys = [ForeignKey( . . . )] )
ما قصد داریم ستون note_folder_id از جدول note_table رو به ستون folder_id از جدول folder_table با استفاده از کلید خارجی در room مرتبط کنیم (توی شماتیک ابتدای مقاله جفتشونو folder_id یاد کردیم، خیلی هم فرقی نمی کنه اسمشون چی باشه، اینکه حتما اسمشون یکی باشه یا نه مهم نیست).
همچنین می خوایم بگیم اگر رکوردی از جدول folders_table که با یک یا چند رکورد از جدول note_table در ارتباط هست، ویرایش شد یا پاک شد؛ چه بلایی سر رکورد های جدول note_table که مرتبط باهاش هستند بیاد.
ForeignKey( entity = FolderEntity::class, parentColumns = arrayOf(FOLDER_ID), childColumns = arrayOf(NOTE_FOLDER_ID), onUpdate = ForeignKey.CASCADE, onDelete = ForeignKey.SET_DEFAULT )
پنج تا پارامتر رو به foreign_key برای کلید خارجی در room اختصاص می دیم. اول از همه می گیم باید با کلاس FolderEntity ارتباط داشته باشی. بعدش می گیم ستون پدر (کلید اصلی در folders_table)، folder_id باشه و ستون فرزند (کلید خارجی در جدول note_table)، note_folder_id باشه(توی شماتیک ابتدای مقاله اسم جفتشونو folder_id یاد کردیم، اینکه نام ها چه باشن مهم نیست).
ولی onUpdate و onDelete چیه؟ خب onUpdate می گه اگر رکوردی از جدول folders_table دچار UPDATE شد چه سیاستی مد نظرته؟ ما می گیم CASCADE ،CASCADE می گه هر اتفاقی سر رکورد های جدول والد اومد من عین همونو سر جدول فرزند می یارم. جدول والد اطلاعاتش تغییر کرده؟ اسم پوشه عوض شده؟رکورد های جدول فرزند هم اسمشون عوض می شه و اسم پوشۀ جدید به اطلاعشون می رسه.
وقتی اسم پوشۀ «کار» رو به «اداره» تغییر دادیم دیدید که؟ یادداشت هایی که توی ستون چپ بودن متوجه این قضیه شدند. حالا فرض کن اصلا جدول folders_table نمی بود؛ با چه دردسی می خواستیم یادداشت ها رو ساماندهی کنیم!
onDelete رو برابر با SET_DEFAULT قرار دادیم. این یعنی من یکی از رکورد هام توی جدول folders_table رو به عنوان پیشفرض قرار دادم. یادداشت هایی که پوشهشون حذف شده رو بجای آنکه با استفاده از سیاست CASCADE متقابلا حذف کنی؛ بیا جزو یک پوشۀ دیگه ای (یک رکورد دیگه ای که پیش فرض هست)، قرار بده.
ما توی برنامهمون یک رکورد در جدول folders_table ایجاد کردیم (اسمش رو هم گذاشتیم بیپوشهها) که توسط کاربر قابل حذف نیست و یادداشت هایی که بخشی از هیچ پوشه ای نیستند؛ توی این پوشه قرار می گیرن.
اما از کجا می دونیم که پوشۀ «بیپوشهها» در واقع همون پوشۀ پیش فرض ماست که یادداشت هایی که پوشهشون حذف شده باید برن توش؟ باید برای ستون note_folder_id که درون جدول note_table هست، چنین پیکر بندی ای انجام بدیم:
..., @ColumnInfo(name = NOTE_FOLDER_ID, defaultValue = "1", index = true) var folder_id :Int = 0 ,...
بهش گفتیم اولین رکوردی که برات ایجاد شد همون رکورد پیش فرض باشه. ما خودمون در ابتدای برنامه رکوردی که اسمش «بیپوشهها» ها هست رو بوجود می یاریم.
به CASCADE و SET_DEFAULT اکشن می گن. اکشن های دیگه ای هم داریم، مثلا SET_NULL مقدار null رو بجای note_folder_id قرار می ده. یا RESTRICT و NO_ACTION دیگر اکشن ها هستند که برای کلید خارجی در room می تونیم استفاده کنیم.
اگر دربارۀ سایر اکشن های SQLite کنجکاو هستی یا دربارۀ آنکه index رو چرا true کردیم کنجکاوی، یک نگاه به این مرجع بنداز.
تا اینجا یاد گریفتیم چطور از اکشن ها برای کلید خارجی در room استفاده کنیم. اما یک سوال! چطور کاری کنیم که هنگام SELECT کردن بتونیم از ارتباط کلید خارجی در room برخوردار بشیم؟
مثلا می خوایم بگیم که همۀ یادداشت ها رو SELECT کن و در اپلیکیشن نشون بده، اما عکس پوشههاشون و اسم پوشههاشونم از قلم ننداز موقعی که SELECTشون می کنی. خب یکم سخته بنظر میاد! من موقع SELECT کردن یادداشت ها یک لیست از NoteEntity دارم و توی پراپرتی های NoteEntity هیچ امکانی برای دریافت عکس و نام پوشه ای که یادداشت توشه برام نیست!
اینجاست که باید از مفهوم relation در پی کلید خارجی در room پرده برداری کنیم! relation برای SELECT کردن از دو جدولی که از قابلیت کلید خارجی در room استفاده می کنن به یاریمون میاد.
مفهوم relation در کلید خارجی در room
برای SELECT کردن ما می یایم توی Dao می گیم یک لیست از NoteEntity برامون SELECT کن؛ یا مثلا طی شرط و شروطی فلان FolderEntity ها با فلان ویژگی ها رو به صورت یک لیست برگردون. مثل موارد زیر:
@Query("SELECT * FROM $FOLDERS_TABLE_NAME") fun getAllFolders() : Flow<MutableList<FolderEntity>>
خب ما فقط دوتا کلاس NoteEntity و FolderEntity رو داشتیم، الان باید یک کلاس سومینی درست بکنیم تا بتونیم حینی که یادداشت ها رو SELECT می کنیم، اسم و عکس پوشه هاشونم SELECT بکنیم:
import androidx.room.Embedded data class NoteAndFolder ( @Embedded val note : NoteEntity, @Embedded val folder: FolderEntity ) { override fun toString(): String { return "عنوان یادداشت : (${note.title})\n"+ "توضیحات یادداشت : (${note.des})\n"+ "تاریخ : (${note.date})\n"+ "ساعت : (${note.time})\n"+ "اولویت : (${note.priority})\n"+ "نام پوشه : (${folder.title})\n"+ if (note.isPinned) "[یادداشت سنجاق شده است.]" else "[یادداشت سنجاق نشده است.]" } }
این کلاس رو توی Dao اگر صدا بزنیم می تونیم به یادداشت ها و پوشه ای که مرتبط بهشونه یک جا با هم دسترسی داشته باشیم؛ اما باید از دستور Inner Join استفاده کنیم که یکی از دستورات SQL برای SELECT کردن از جدول هایی هست که با هم ارتباط دارند(در اینجا به کمک کلید خارجی در room این ارتباط میان دو تا جدولمون وجود داره).
پس الان که از کلید خارجی در room استفاده می کنیم؛ امکان استفاده از Inner Join هم برامون فراهمه.
@Transaction @Query("SELECT $NOTE_TABLE_NAME.* ,$FOLDERS_TABLE_NAME.* FROM $NOTE_TABLE_NAME " + "INNER JOIN $FOLDERS_TABLE_NAME ON $NOTE_FOLDER_ID = $FOLDER_ID " + "ORDER BY note_is_pinned DESC") fun getAllNotesRelated(): Flow<MutableList<NoteAndFolder>>
از تراکنش (Transaction) برای تابعمون استفاده کردیم؛ همچنین چون از Inner Join استفاده کردیم، می تونیم با خیال راحت از NoteAndFolder به عنوان مدل، برای دریافت اطلاعات استفاده کنیم؛ حالا با خیال راحت می تونیم از کلاس NoteAndFolder برای دریافت لیستی از یادداشت ها همراه با رکورد های پوشه هایی که باهاشون در ارتباطن استفاده کنیم.
این تابع یک لیست از یادداشت ها رو بر می گردونه که اطلاعات پوشۀ مرتبط باهاشونم همراهشون هست.
کلاس NoteAndFolder از انوتیشن Embedded استفاده می کنه، این انوتیشن به معنای تعبیه کردن هست. یعنی دوتا کلاس NoteEntity و FolderEntity با هم یک کاسه می شه.
ما از Flow برای دریافت اطلاعات استفاده می کنیم (اگر هیچی دربارۀ flow نمی دونی این مقاله رو بخون). استفاده از flow برای room باعث می شه تا به محض کوچترین تغییری در اطلاعات پایگاه داده، ui ما بروزرسانی بشه.
این پروژه که توی این مقاله معرفی کردم از معماری mvvm استفاده می کنه و من به عنوان شاگرد استاد نوری در دورۀ نخبگان اندروید؛ این پروژه رو به صورت متن باز و با ایده ها و ابتکار های خودم بعد از فصل سوم دوره، به عنوان تمرین توسعه دادم. این پروژه به صورت متن باز در این لینک گیت هاب موجوده.
همچنین تابع toString کلاس NoteAndFolder رو بازنویسی کردیم تا اگر شئ ای از این کلاس رو تبدیل به String کردیم؛ دقیقا عین محتوای فایل txt ای که در عکس های ابتدای مقاله دیدید در بیاد! پس با استفاده از تابع toString تمام اطلاعات یادداشتمونو به صورت یک رشته در می یاریم و توی یک فایل txt می نویسیم و داخل حافظۀ گوشی ذخیره می کنیم
بدین ترتیب راحت اطلاعات ما از قالب شئ گرایی به قالب فایل متنی برای یادداشت ها و پوشه های واقعی درون حافظۀ گوشی قابل دگرگشت هستند!
اگر دربارۀ تراکش یا Inner Jion یا سایر نکات SQL ای یا جزئیات بیشتر دربارۀ کلید خارجی و ارتباط یک به چند جداول و… دوست دارید بیشتر بدونید؛ یادگیری SQL در حد متوسط و آشنایی با انواع متخلف دستوراتش، خالی از لطف نمی تونه باشه. این می تونه باعث بشه از کد نویسی های اضافی برای پردازش اطلاعات آفلاین اپلیکیشنتون پرهیز بکنید.
ویدیوی اپلیکیشن رو در ذیل لحاظ بفرمایید:
سورس کد پروژه رو قرار دادم میتونید دانلود کنید، اگر اپلیکیشن رو هم خواستید دانلود کنید از لینک زیر میتونید استفاده کنید.