عند إضافة ميزات وتغييرها في تطبيقك، عليك تعديل فئات كيان Room وجداول قاعدة البيانات الأساسية لتعكس هذه التغييرات. من المهم الاحتفاظ ببيانات المستخدمين المخزّنة حاليًا في قاعدة البيانات على الجهاز فقط عندما يغيّر أحد تحديثات التطبيق مخطط قاعدة البيانات.
تتيح 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 { ... }
مواصفات عمليات النقل التلقائية
إذا رصدت Room تغييرات غامضة في المخطط ولم تتمكّن من إنشاء
خطة نقل بدون مزيد من المعلومات، ستعرض خطأ في وقت التجميع وتطلب منك
تنفيذ
AutoMigrationSpec.
يحدث ذلك في أغلب الأحيان عندما تتضمّن عملية النقل أحد الإجراءَين التاليَين:
- حذف جدول أو إعادة تسميته
- حذف عمود أو إعادة تسميته
يمكنك استخدام AutoMigrationSpec لتزويد Room بالمعلومات الإضافية التي تحتاج إليها لإنشاء مسارات النقل بشكل صحيح. حدِّد فئة ثابتة تنفّذ 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، ستطلبها Room بعد اكتمال عملية النقل التلقائية.
عمليات النقل اليدوية
في الحالات التي تتضمّن فيها عملية النقل تغييرات معقّدة في المخطط، قد لا تتمكّن Room من إنشاء مسار نقل مناسب تلقائيًا. على سبيل المثال، إذا قرّرت تقسيم البيانات في جدول إلى جدولَين، لا يمكن لـ Room تحديد كيفية إجراء هذا التقسيم. في حالات كهذه، عليك تحديد مسار نقل يدويًا من خلال تنفيذ فئة
Migration.
تحدّد فئة Migration بشكل صريح مسار نقل بين
startVersion وendVersion من خلال إلغاء طريقة
Migration.migrate(). أضِف فئات Migration إلى أداة إنشاء قاعدة البيانات باستخدام
طريقة
addMigrations():
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 عملية النقل اليدوية.
اختبار عمليات النقل
غالبًا ما تكون عمليات النقل معقّدة، ويمكن أن يؤدي تحديد عملية نقل بشكل غير صحيح إلى تعطُّل تطبيقك. للحفاظ على استقرار تطبيقك، اختبِر عمليات النقل. توفّر Room عنصر Maven room-testing للمساعدة في عملية الاختبار لكلّ من عمليات النقل التلقائية واليدوية. لكي يعمل هذا العنصر، عليك أولاً تصدير مخطط قاعدة البيانات.
تصدير المخططات
يمكن لـ Room تصدير معلومات مخطط قاعدة البيانات إلى ملف JSON في وقت التجميع. تمثّل ملفات JSON التي تم تصديرها سجلّ مخطط قاعدة البيانات. خزِّن هذه الملفات في نظام التحكّم في الإصدارات حتى تتمكّن Room من إنشاء إصدارات أقدم من قاعدة البيانات لأغراض الاختبار ولتفعيل إنشاء عمليات النقل التلقائية.
ضبط موقع المخطط باستخدام Room Gradle Plugin
إذا كنت تستخدم الإصدار 2.6.0 من Room أو إصدارًا أحدث، يمكنك تطبيق الـ
Room Gradle Plugin واستخدام الـ
room لتحديد دليل المخطط.
أنيق
plugins {
id 'androidx.room'
}
room {
schemaDirectory "$projectDir/schemas"
}
Kotlin
plugins {
id("androidx.room")
}
room {
schemaDirectory("$projectDir/schemas")
}
إذا كان مخطط قاعدة البيانات يختلف استنادًا إلى المتغيّر أو النكهة أو نوع الإصدار، عليك تحديد مواقع مختلفة باستخدام إعداد schemaDirectory() عدة مرات، مع استخدام variantMatchName كحجة أولى في كل مرة. يمكن أن يتطابق كل إعداد مع متغيّر واحد أو أكثر استنادًا إلى مقارنة بسيطة مع اسم المتغيّر.
تأكَّد من أنّ هذه الإعدادات شاملة وتغطّي جميع المتغيّرات. يمكنك أيضًا تضمين schemaDirectory() بدون variantMatchName للتعامل مع المتغيّرات التي لا تتطابق مع أي من الإعدادات الأخرى. على سبيل المثال، في تطبيق يتضمّن نكهتَي إصدار demo وfull ونوعَي إصدار 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 أو إصدارًا أقدم، أو إذا كنت لا تستخدم Room Gradle Plugin، اضبط موقع المخطط باستخدام خيار معالج التعليقات التوضيحية room.schemaLocation.
تُستخدَم الملفات في هذا الدليل كمدخلات ومخرجات لبعض مهام Gradle.
لضمان صحة وأداء عمليات الإنشاء المتزايدة والمخزّنة مؤقتًا، عليك استخدام
Gradle's
CommandLineArgumentProvider
لإعلام Gradle بهذا الدليل.
أولاً، انسخ فئة RoomSchemaArgProvider الموضّحة أدناه إلى ملف 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"))
)
}
}
}
}
اختبار عملية نقل واحدة
قبل أن تتمكّن من اختبار عمليات النقل، أضِف عنصر Maven androidx.room:room-testing من Room إلى تبعيات الاختبار وأضِف موقع المخطط الذي تم تصديره كدليل مواد عرض:
build.gradle
أنيق
android { ... sourceSets { // Adds exported schema location as test app assets. androidTest.assets.srcDirs += files("$projectDir/schemas".toString()) } } dependencies { ... androidTestImplementation "androidx.room:room-testing:2.8.4" }
Kotlin
android { ... sourceSets { // Adds exported schema location as test app assets. getByName("androidTest").assets.srcDir("$projectDir/schemas") } } dependencies { ... testImplementation("androidx.room:room-testing:2.8.4") }
توفر حزمة الاختبار فئة
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 إعادة إنشاء الجداول بشكل مدمّر في قاعدة بيانات تطبيقك عندما تحتاج إلى إجراء عملية نقل تدريجية ولا يتوفّر مسار نقل محدّد.
إذا كنت تريد أن تعود Room إلى إعادة الإنشاء بشكل مدمّر في حالات معيّنة فقط، هناك بعض البدائل لـ fallbackToDestructiveMigration():
- إذا كانت إصدارات معيّنة من سجلّ المخطط تتسبّب في حدوث أخطاء لا يمكنك حلّها
باستخدام مسارات النقل، استخدِم
fallbackToDestructiveMigrationFrom()بدلاً من ذلك. تشير هذه الطريقة إلى أنّك تريد أن تعود Room إلى إعادة الإنشاء بشكل مدمّر فقط عند النقل من إصدارات معيّنة. - إذا كنت تريد أن تعود Room إلى إعادة الإنشاء بشكل مدمّر فقط عند النقل
من إصدار قاعدة بيانات أعلى إلى إصدار أقل، استخدِم
fallbackToDestructiveMigrationOnDowngrade()بدلاً من ذلك.
التعامل مع القيم التلقائية للأعمدة عند الترقية إلى Room 2.2.0
في الإصدار 2.2.0 من Room والإصدارات الأحدث، يمكنك تحديد قيمة تلقائية لعمود باستخدام
التعليق التوضيحي
@ColumnInfo(defaultValue = "...").
في الإصدارات الأقدم من 2.2.0، الطريقة الوحيدة لتحديد قيمة تلقائية لعمود هي تحديدها مباشرةً في عبارة SQL يتم تنفيذها، ما يؤدي إلى إنشاء قيمة تلقائية لا تعرفها Room. يعني ذلك أنّه إذا تم إنشاء قاعدة بيانات في الأصل باستخدام إصدار من Room أقدم من 2.2.0، قد تتطلب ترقية تطبيقك لاستخدام Room 2.2.0 منك توفير مسار نقل خاص للقيم التلقائية الحالية التي حدّدتها بدون استخدام واجهات برمجة تطبيقات 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 في مخطط قاعدة البيانات.
في إصدارات Room الأقدم من 2.2.0، يكون هذا التناقض غير ضار. ومع ذلك، إذا
تم لاحقًا ترقية التطبيق لاستخدام Room 2.2.0 أو إصدار أحدث وتغيير فئة كيان Song لتضمين قيمة تلقائية لـ tag باستخدام التعليق التوضيحي
@ColumnInfo، يمكن لـ Room
عندئذٍ رصد هذا التناقض. يؤدي ذلك إلى فشل عمليات التحقق من صحة المخطط.
للمساعدة في ضمان اتساق مخطط قاعدة البيانات لدى جميع المستخدمين عند الإعلان عن القيم التلقائية للأعمدة في مسارات النقل السابقة، اتّبِع الخطوات التالية في المرة الأولى التي ترقّي فيها تطبيقك لاستخدام Room 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"); } };