כשמוסיפים תכונות באפליקציה ומשנים אותן, צריך לשנות את הישות לחדר והטבלאות של מסדי הנתונים הבסיסיים כדי לשקף את השינויים האלה. חשוב כדי לשמור נתוני משתמשים שכבר נמצאים במסד הנתונים במכשיר כשאפליקציה משנה את הסכימה של מסד הנתונים.
Room תומך באפשרויות אוטומטיות וידניות להעברה מצטברת. העברות אוטומטיות פועלות עבור רוב השינויים הבסיסיים בסכימה, אבל ייתכן שיהיה עליך להגדיר באופן ידני נתיבי העברה לשינויים מורכבים יותר.
העברות אוטומטיות
כדי להצהיר על העברה אוטומטית בין שתי גרסאות של מסד נתונים, צריך להוסיף
@AutoMigration
הערה ל
autoMigrations
נכס ב-@Database
:
Kotlin
// Database class before the version update. @Database( version = 1, entities = [User::class] ) abstract class AppDatabase : RoomDatabase() { ... } // Database class after the version update. @Database( version = 2, entities = [User::class], autoMigrations = [ AutoMigration (from = 1, to = 2) ] ) abstract class AppDatabase : RoomDatabase() { ... }
Java
// Database class before the version update. @Database( version = 1, entities = {User.class} ) public abstract class AppDatabase extends RoomDatabase { ... } // Database class after the version update. @Database( version = 2, entities = {User.class}, autoMigrations = { @AutoMigration (from = 1, to = 2) } ) public abstract class AppDatabase extends RoomDatabase { ... }
מפרטי העברה אוטומטית
אם החדר מזהה שינויים לא ברורים בסכימה והוא לא יכול ליצור
תוכנית למיגרציה ללא קלט נוסף, היא גורמת לשגיאה זמן הידור (compiler) ושואלת
כדי ליישם
AutoMigrationSpec
בדרך כלל מצב כזה מתרחש כשהעברה כוללת אחד מהמצבים הבאים:
- מחיקה או שינוי שם של טבלה.
- מחיקה או שינוי שם של עמודה.
אפשר להשתמש באפליקציית AutoMigrationSpec
כדי לספק לחדר את המידע הנוסף
צריך ליצור נתיבי העברה בצורה תקינה. הגדירו מחלקה סטטית
מטמיעים את AutoMigrationSpec
בכיתה RoomDatabase
ומוסיפים לו הערות
אחד או יותר מהאפשרויות הבאות:
כדי להשתמש בהטמעה של AutoMigrationSpec
להעברה אוטומטית, צריך להגדיר
המאפיין spec
בהערה המתאימה ב-@AutoMigration
:
Kotlin
@Database( version = 2, entities = [User::class], autoMigrations = [ AutoMigration ( from = 1, to = 2, spec = AppDatabase.MyAutoMigration::class ) ] ) abstract class AppDatabase : RoomDatabase() { @RenameTable(fromTableName = "User", toTableName = "AppUser") class MyAutoMigration : AutoMigrationSpec ... }
Java
@Database( version = 2, entities = {AppUser.class}, autoMigrations = { @AutoMigration ( from = 1, to = 2, spec = AppDatabase.MyAutoMigration.class ) } ) public abstract class AppDatabase extends RoomDatabase { @RenameTable(fromTableName = "User", toTableName = "AppUser") static class MyAutoMigration implements AutoMigrationSpec { } ... }
אם האפליקציה שלך תצטרך לבצע פעולות נוספות לאחר השלמת ההעברה האוטומטית, אתה
יכול ליישם
onPostMigrate()
אם מטמיעים את השיטה הזו ב-AutoMigrationSpec
, החדר יתקשר אליה אחרי
תהליך ההעברה האוטומטי מסתיים.
העברות ידניות
במקרים שבהם ההעברה כרוכה בשינויים מורכבים בסכימה, יכול להיות שהחדר לא יהיה
ליצור נתיב העברה מתאים באופן אוטומטי. לדוגמה, אם
אתם מחליטים לפצל את הנתונים בטבלה לשתי טבלאות, 'חדר' לא יכול לדעת
איך לבצע את הפיצול הזה. במקרים כאלה, צריך לבצע את הפעולות הבאות באופן ידני
להגדיר נתיב העברה באמצעות
Migration
.
מחלקה Migration
מגדירה באופן מפורש נתיב העברה בין
startVersion
ו-endVersion
על ידי ביטול של
Migration.migrate()
. הוספת הכיתות של Migration
לבונה מסדי נתונים באמצעות
ה
addMigrations()
method:
Kotlin
val MIGRATION_1_2 = object : Migration(1, 2) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL("CREATE TABLE `Fruit` (`id` INTEGER, `name` TEXT, " + "PRIMARY KEY(`id`))") } } val MIGRATION_2_3 = object : Migration(2, 3) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL("ALTER TABLE Book ADD COLUMN pub_year INTEGER") } } Room.databaseBuilder(applicationContext, MyDb::class.java, "database-name") .addMigrations(MIGRATION_1_2, MIGRATION_2_3).build()
Java
static final Migration MIGRATION_1_2 = new Migration(1, 2) { @Override public void migrate(SupportSQLiteDatabase database) { database.execSQL("CREATE TABLE `Fruit` (`id` INTEGER, " + "`name` TEXT, PRIMARY KEY(`id`))"); } }; static final Migration MIGRATION_2_3 = new Migration(2, 3) { @Override public void migrate(SupportSQLiteDatabase database) { database.execSQL("ALTER TABLE Book " + " ADD COLUMN pub_year INTEGER"); } }; Room.databaseBuilder(getApplicationContext(), MyDb.class, "database-name") .addMigrations(MIGRATION_1_2, MIGRATION_2_3).build();
כשמגדירים את נתיבי ההעברה, אפשר להשתמש בהעברות אוטומטיות והעברות ידניות לאחרים. אם מגדירים גם תבנית עריכה אוטומטית העברה ידנית והעברה ידנית עבור אותה גרסה, אז Room משתמש ב מיגרציה.
בדיקת העברות
העברות הן לעיתים קרובות מורכבות, והעברה שמוגדרת בצורה שגויה עלולה לגרום
כדי שהאפליקציה תקרוס. כדי לשמור על היציבות של האפליקציה, כדאי לבדוק את
והעברות. בחדר יש ארטיפקט של Maven מהroom-testing
כדי לעזור
תהליך בדיקה להעברות אוטומטיות וידניות. עבור פריט המידע הזה שנוצר בתהליך הפיתוח (Artifact) כדי
צריך לייצא תחילה את הסכימה של מסד הנתונים.
ייצוא סכימות
חדר יכול לייצא את פרטי הסכימה של מסד הנתונים לקובץ JSON במהלך הידור בזמן האימון. קובצי ה-JSON שתייצאו מייצגים את היסטוריית הסכימה של מסד הנתונים. חנות קבצים אלה במערכת ניהול הגרסאות שלכם כדי שחדר יוכל ליצור גרסאות נמוכות יותר של מסד הנתונים למטרות בדיקה וכדי לאפשר יצירת העברה אוטומטית.
הגדרת מיקום הסכימה באמצעות הפלאגין Room Gradle
אם אתם משתמשים בגרסה 2.6.0 ומעלה של החדר, אתם יכולים להחיל את
פלאגין Room Gradle ושימוש ב-
התוסף room
לציון ספריית הסכימות.
מגניב
plugins {
id 'androidx.room'
}
room {
schemaDirectory "$projectDir/schemas"
}
Kotlin
plugins {
id("androidx.room")
}
room {
schemaDirectory("$projectDir/schemas")
}
אם הסכימה של מסד הנתונים משתנה בהתאם לווריאנט, לטעם או ל-build
מהסוג הזה, יש לציין מיקומים שונים באמצעות הפרמטר schemaDirectory()
כמה פעמים, כשכל אחת היא variantMatchName
ארגומנט. כל הגדרה יכולה להתאים לווריאנט אחד או יותר לפי תקן
להשוואה לשם הווריאנט.
חשוב לוודא שכל המסמכים מפורטים ומכסים את כל הווריאציות. אפשר גם לכלול
schemaDirectory()
בלי variantMatchName
כדי לטפל בווריאציות שלא נמצאה להן התאמה
כל הגדרה אחרת. לדוגמה, באפליקציה שיש בה שני גרסאות build
demo
ו-full
ושני סוגי build debug
ו-release
,
אלה ההגדרות החוקיות:
מגניב
room {
// Applies to 'demoDebug' only
schemaDirectory "demoDebug", "$projectDir/schemas/demoDebug"
// Applies to 'demoDebug' and 'demoRelease'
schemaDirectory "demo", "$projectDir/schemas/demo"
// Applies to 'demoDebug' and 'fullDebug'
schemaDirectory "debug", "$projectDir/schemas/debug"
// Applies to variants that aren't matched by other configurations.
schemaDirectory "$projectDir/schemas"
}
Kotlin
room {
// Applies to 'demoDebug' only
schemaDirectory("demoDebug", "$projectDir/schemas/demoDebug")
// Applies to 'demoDebug' and 'demoRelease'
schemaDirectory("demo", "$projectDir/schemas/demo")
// Applies to 'demoDebug' and 'fullDebug'
schemaDirectory("debug", "$projectDir/schemas/debug")
// Applies to variants that aren't matched by other configurations.
schemaDirectory("$projectDir/schemas")
}
הגדרת מיקום הסכימה באמצעות אפשרות של מעבד הערות
אם אתם משתמשים בגרסה 2.5.2 ומטה של 'חדר', או אם אתם לא משתמשים
פלאגין Room Gradle, הגדרת מיקום הסכימה באמצעות room.schemaLocation
לעיבוד הערות.
הקבצים בספרייה הזו משמשים כקלט וכפלט למשימות מסוימות של Gradle.
כדי לבדוק את הנכונות והביצועים של גרסאות build מצטברות ושמורות במטמון, צריך להשתמש
של Gradle
CommandLineArgumentProvider
כדי לעדכן את Gradle לגבי הספרייה הזו.
קודם כול, מעתיקים את הכיתה RoomSchemaArgProvider
שמוצגת למטה לתוך המודול
קובץ build של Gradle. השיטה asArguments()
בכיתה לדוגמה עוברת
room.schemaLocation=${schemaDir.path}
עד KSP
. אם אתם משתמשים ב-KAPT
וב
javac
, אפשר לשנות את הערך הזה ל--Aroom.schemaLocation=${schemaDir.path}
במקום זאת.
מגניב
class RoomSchemaArgProvider implements CommandLineArgumentProvider {
@InputDirectory
@PathSensitive(PathSensitivity.RELATIVE)
File schemaDir
RoomSchemaArgProvider(File schemaDir) {
this.schemaDir = schemaDir
}
@Override
Iterable<String> asArguments() {
// Note: If you're using KAPT and javac, change the line below to
// return ["-Aroom.schemaLocation=${schemaDir.path}".toString()].
return ["room.schemaLocation=${schemaDir.path}".toString()]
}
}
Kotlin
class RoomSchemaArgProvider(
@get:InputDirectory
@get:PathSensitive(PathSensitivity.RELATIVE)
val schemaDir: File
) : CommandLineArgumentProvider {
override fun asArguments(): Iterable<String> {
// Note: If you're using KAPT and javac, change the line below to
// return listOf("-Aroom.schemaLocation=${schemaDir.path}").
return listOf("room.schemaLocation=${schemaDir.path}")
}
}
לאחר מכן צריך להגדיר את אפשרויות ההידור כך שישתמשו ב-RoomSchemaArgProvider
עם
ספריית הסכימה שצוינה:
מגניב
// For KSP, configure using KSP extension:
ksp {
arg(new RoomSchemaArgProvider(new File(projectDir, "schemas")))
}
// For javac or KAPT, configure using android DSL:
android {
...
defaultConfig {
javaCompileOptions {
annotationProcessorOptions {
compilerArgumentProviders(
new RoomSchemaArgProvider(new File(projectDir, "schemas"))
)
}
}
}
}
Kotlin
// For KSP, configure using KSP extension:
ksp {
arg(RoomSchemaArgProvider(File(projectDir, "schemas")))
}
// For javac or KAPT, configure using android DSL:
android {
...
defaultConfig {
javaCompileOptions {
annotationProcessorOptions {
compilerArgumentProviders(
RoomSchemaArgProvider(File(projectDir, "schemas"))
)
}
}
}
}
בדיקת העברה יחידה
כדי לבדוק את ההעברות, צריך להוסיף את
androidx.room:room-testing
ארטיפקט של Maven מהחדר לבדיקה
של יחסי התלות ומוסיפים את המיקום של הסכימה המיוצאת כתיקיית נכסים:
מגניב
android { ... sourceSets { // Adds exported schema location as test app assets. androidTest.assets.srcDirs += files("$projectDir/schemas".toString()) } } dependencies { ... androidTestImplementation "androidx.room:room-testing:2.6.1" }
Kotlin
android { ... sourceSets { // Adds exported schema location as test app assets. getByName("androidTest").assets.srcDir("$projectDir/schemas") } } dependencies { ... testImplementation("androidx.room:room-testing:2.6.1") }
חבילת הבדיקה מספקת
MigrationTestHelper
, שיכול לקרוא קובצי סכימה שיוצאו. החבילה גם מטמיעה את
JUnit4
TestRule
כדי לנהל מסדי נתונים שנוצרו.
הדוגמה הבאה ממחישה בדיקה של העברה אחת:
Kotlin
@RunWith(AndroidJUnit4::class) class MigrationTest { private val TEST_DB = "migration-test" @get:Rule val helper: MigrationTestHelper = MigrationTestHelper( InstrumentationRegistry.getInstrumentation(), MigrationDb::class.java.canonicalName, FrameworkSQLiteOpenHelperFactory() ) @Test @Throws(IOException::class) fun migrate1To2() { var db = helper.createDatabase(TEST_DB, 1).apply { // Database has schema version 1. Insert some data using SQL queries. // You can't use DAO classes because they expect the latest schema. execSQL(...) // Prepare for the next version. close() } // Re-open the database with version 2 and provide // MIGRATION_1_2 as the migration process. db = helper.runMigrationsAndValidate(TEST_DB, 2, true, MIGRATION_1_2) // MigrationTestHelper automatically verifies the schema changes, // but you need to validate that the data was migrated properly. } }
Java
@RunWith(AndroidJUnit4.class) public class MigrationTest { private static final String TEST_DB = "migration-test"; @Rule public MigrationTestHelper helper; public MigrationTest() { helper = new MigrationTestHelper(InstrumentationRegistry.getInstrumentation(), MigrationDb.class.getCanonicalName(), new FrameworkSQLiteOpenHelperFactory()); } @Test public void migrate1To2() throws IOException { SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 1); // Database has schema version 1. Insert some data using SQL queries. // You can't use DAO classes because they expect the latest schema. db.execSQL(...); // Prepare for the next version. db.close(); // Re-open the database with version 2 and provide // MIGRATION_1_2 as the migration process. db = helper.runMigrationsAndValidate(TEST_DB, 2, true, MIGRATION_1_2); // MigrationTestHelper automatically verifies the schema changes, // but you need to validate that the data was migrated properly. } }
בדיקת כל ההעברות
למרות שאפשר לבדוק מיגרציה מצטברת אחת, מומלץ תכלול בדיקה שתכסה את כל ההעברות שהוגדרו עבור האפליקציה מסד נתונים. כך אפשר לוודא שאין אי-התאמה בין קובץ שנוצר לאחרונה במופע של מסד נתונים ובמכונה ישנה יותר שבעקבותיה התרחשה ההעברה שהוגדרה .
הדוגמה הבאה מציגה בדיקה של כל ההעברות המוגדרות:
Kotlin
@RunWith(AndroidJUnit4::class) class MigrationTest { private val TEST_DB = "migration-test" // Array of all migrations. private val ALL_MIGRATIONS = arrayOf( MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4) @get:Rule val helper: MigrationTestHelper = MigrationTestHelper( InstrumentationRegistry.getInstrumentation(), AppDatabase::class.java.canonicalName, FrameworkSQLiteOpenHelperFactory() ) @Test @Throws(IOException::class) fun migrateAll() { // Create earliest version of the database. helper.createDatabase(TEST_DB, 1).apply { close() } // Open latest version of the database. Room validates the schema // once all migrations execute. Room.databaseBuilder( InstrumentationRegistry.getInstrumentation().targetContext, AppDatabase::class.java, TEST_DB ).addMigrations(*ALL_MIGRATIONS).build().apply { openHelper.writableDatabase.close() } } }
Java
@RunWith(AndroidJUnit4.class) public class MigrationTest { private static final String TEST_DB = "migration-test"; @Rule public MigrationTestHelper helper; public MigrationTest() { helper = new MigrationTestHelper(InstrumentationRegistry.getInstrumentation(), AppDatabase.class.getCanonicalName(), new FrameworkSQLiteOpenHelperFactory()); } @Test public void migrateAll() throws IOException { // Create earliest version of the database. SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 1); db.close(); // Open latest version of the database. Room validates the schema // once all migrations execute. AppDatabase appDb = Room.databaseBuilder( InstrumentationRegistry.getInstrumentation().getTargetContext(), AppDatabase.class, TEST_DB) .addMigrations(ALL_MIGRATIONS).build(); appDb.getOpenHelper().getWritableDatabase(); appDb.close(); } // Array of all migrations. private static final Migration[] ALL_MIGRATIONS = new Migration[]{ MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4}; }
טיפול יעיל בנתיבי העברה חסרים
אם Room לא מוצא נתיב העברה לשדרוג מסד נתונים קיים
את המכשיר לגרסה הנוכחית,
IllegalStateException
. אם המיקום
אפשר לאבד נתונים קיימים כאשר נתיב העברה חסר,
ה
fallbackToDestructiveMigration()
כאשר יוצרים את מסד הנתונים:
Kotlin
Room.databaseBuilder(applicationContext, MyDb::class.java, "database-name") .fallbackToDestructiveMigration() .build()
Java
Room.databaseBuilder(getApplicationContext(), MyDb.class, "database-name") .fallbackToDestructiveMigration() .build();
השיטה הזו מנחה את Room ליצור מחדש באופן הרסני את הטבלאות באפליקציה כשצריך לבצע מיגרציה מצטברת ואין נתיב העברה מוגדר.
אם מעדיפים להשתמש ב'חדר' רק כדי לחזור וליהנות מפעילויות הרסניות מסוגים מסוימים
במצבים מסוימים, יש כמה חלופות ל-fallbackToDestructiveMigration()
:
- אם גרסאות ספציפיות של היסטוריית הסכימות גורמות לשגיאות שאין לכם אפשרות לפתור
עם נתיבי העברה, משתמשים
fallbackToDestructiveMigrationFrom()
במקום זאת. השיטה הזו מציינת שאתם רוצים שהחדר יחזור להיות הרסני רק כשעוברים מגרסאות ספציפיות. - אם רוצים שחדר הרחצה יוכל לחזור וליהנות מפעילויות הרסניות רק לאחר ההעברה
מגרסה גבוהה יותר של מסד נתונים לגרסה נמוכה יותר, משתמשים
fallbackToDestructiveMigrationOnDowngrade()
במקום זאת.
כשמשדרגים לחדר 2.2.0, צריך לטפל בערכי ברירת המחדל של העמודות
בחדר 2.2.0 ואילך אפשר להגדיר ערך ברירת מחדל לעמודה באמצעות
ההערה
@ColumnInfo(defaultValue = "...")
בגרסאות שקודמות לגרסה 2.2.0, הדרך היחידה להגדיר ערך ברירת מחדל
היא להגדיר אותה ישירות בהצהרת SQL שמופעלת, ותיצור
ערך ברירת המחדל שהחדר לא מכיר. כלומר, אם מסד נתונים
נוצרה במקור על ידי גרסה של 'חדר' ישנה יותר מ-2.2.0, ושדרגתי את האפליקציה שלך
יכול להיות שתצטרכו לספק נתיב העברה מיוחד כדי להשתמש ב'חדר 2.2.0'.
ערכי ברירת מחדל קיימים שהגדרתם בלי להשתמש בממשקי API של Room.
לדוגמה, נניח שגרסה 1 של מסד נתונים מגדירה ישות Song
:
Kotlin
// Song entity, database version 1, Room 2.1.0. @Entity data class Song( @PrimaryKey val id: Long, val title: String )
Java
// Song entity, database version 1, Room 2.1.0. @Entity public class Song { @PrimaryKey final long id; final String title; }
נניח גם שגרסה 2 של אותו מסד נתונים מוסיפה עמודת NOT NULL
חדשה
ומגדיר נתיב העברה מגרסה 1 לגרסה 2:
Kotlin
// Song entity, database version 2, Room 2.1.0. @Entity data class Song( @PrimaryKey val id: Long, val title: String, val tag: String // Added in version 2. ) // Migration from 1 to 2, Room 2.1.0. val MIGRATION_1_2 = object : Migration(1, 2) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL( "ALTER TABLE Song ADD COLUMN tag TEXT NOT NULL DEFAULT ''") } }
Java
// Song entity, database version 2, Room 2.1.0. @Entity public class Song { @PrimaryKey final long id; final String title; @NonNull final String tag; // Added in version 2. } // Migration from 1 to 2, Room 2.1.0. static final Migration MIGRATION_1_2 = new Migration(1, 2) { @Override public void migrate(SupportSQLiteDatabase database) { database.execSQL( "ALTER TABLE Song ADD COLUMN tag TEXT NOT NULL DEFAULT ''"); } };
הדבר גורם לפערים בטבלה הבסיסית בין עדכונים לבין נתונים חדשים
התקנות של האפליקציה. כי ערך ברירת המחדל של העמודה tag
הוא רק
מוצהר בנתיב ההעברה מגרסה 1 לגרסה 2, כל המשתמשים
להתקין את האפליקציה החל מגרסה 2 לא כוללים את ערך ברירת המחדל עבור tag
בסכימה של מסד הנתונים.
בגרסאות של 'חדר' שקודמות לגרסה 2.2.0, הפער הזה לא מזיק. אבל אם
האפליקציה תשדרג מאוחר יותר לשימוש בחדר 2.2.0 ואילך ותשנה את הישות Song
class כדי לכלול ערך ברירת מחדל עבור tag
באמצעות הפונקציה
הערה @ColumnInfo
, חדר
ואז נוכל לראות את הפער. התוצאה היא סכימה נכשלה
אימותים חדשים.
כדי להבטיח שסכימת מסד הנתונים עקבית בין כל המשתמשים כאשר העמודה ערכי ברירת המחדל מוצהרים בנתיבי ההעברה הקודמים, יש לבצע את הפעולות הבאות בפעם הראשונה שמשדרגים את האפליקציה לשימוש בחדר 2.2.0 ואילך:
- להצהיר על ערכי ברירת מחדל של עמודות במחלקות הישויות המתאימות באמצעות המאפיין
הערה אחת (
@ColumnInfo
). - צריך להגדיל ב-1 את מספר הגרסה של מסד הנתונים.
- להגדיר נתיב העברה לגרסה החדשה שמממשת את ההשקה ו יצירה מחדש של האסטרטגיה כדי להוסיף את ערכי ברירת המחדל הנדרשים לעמודות הקיימות.
הדוגמה הבאה ממחישה את התהליך הזה:
Kotlin
// Migration from 2 to 3, Room 2.2.0. val MIGRATION_2_3 = object : Migration(2, 3) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL(""" CREATE TABLE new_Song ( id INTEGER PRIMARY KEY NOT NULL, name TEXT, tag TEXT NOT NULL DEFAULT '' ) """.trimIndent()) database.execSQL(""" INSERT INTO new_Song (id, name, tag) SELECT id, name, tag FROM Song """.trimIndent()) database.execSQL("DROP TABLE Song") database.execSQL("ALTER TABLE new_Song RENAME TO Song") } }
Java
// Migration from 2 to 3, Room 2.2.0. static final Migration MIGRATION_2_3 = new Migration(2, 3) { @Override public void migrate(SupportSQLiteDatabase database) { database.execSQL("CREATE TABLE new_Song (" + "id INTEGER PRIMARY KEY NOT NULL," + "name TEXT," + "tag TEXT NOT NULL DEFAULT '')"); database.execSQL("INSERT INTO new_Song (id, name, tag) " + "SELECT id, name, tag FROM Song"); database.execSQL("DROP TABLE Song"); database.execSQL("ALTER TABLE new_Song RENAME TO Song"); } };