android 从sqlite迁移到房间数据库

osh3o9ms  于 2023-02-20  发布在  Android
关注(0)|答案(2)|浏览(119)

如果我们从SQLite移到房间数据库,数据库是否保持完整。数据库中的所有条目是否保持完整。模式是否保持不变。

ni65a41a

ni65a41a2#

所有的数据都可以被保留,但是,实际的模式可能需要改变,因为Room对列类型有限制。
Room仅接受特定的列类型INTEGERREALTEXTBLOB
但是,SQLite具有灵活的列类型,并使用一组简单的规则来分配类型关联。

  • 例如CREATE TABLE mytable (mycolumn CHAR(10)....),根据规则,即使类型亲和性是TEXT,Room也不会接受CHAR(10)。因此,必须更改模式,并且数据库的迁移必须反映这一点。

room也期望一些约束,特别是NOT NULL是精确的。room中的NOT NULL约束通常是隐含的,例如在Java中,一个原语在Kotlin上隐式地为NOT NULL,它没有原语,总是对象,考虑类型是否可以为空,即指定可空性的**?**。
确定房间到底期望什么可能是一场噩梦。然而,它不一定是房间可以帮助的。
之后

  • a)创建所提出的@Entity(表)注解类,以及
  • b)在所述@Database注解的entities参数中定义所述@Entity注解的类,以及
  • c)然后成功编译项目(CTRL + F9)

那么Room处理将生成一些Java代码(即使使用Kotlin)。这将位于java(生成的)文件夹/目录(很容易从Android视图中查看).在生成的java类中,它与@Database注解类同名,但后缀为**_Impl**,将有一个方法名**createAllTables**。这是符合Room预期的SQL,可用于将数据库从旧模式转换为新模式。

    • 演示**

考虑以下应用程序(房间转换前):-
扩展SQLiteOpenHelper的DBHelper类:-

class DBHelper extends SQLiteOpenHelper {

   private static final String DATABASE_NAME = "the_database.db";
   private static final int DATABASE_VERSION = 1;

   public static final String USER_TABLE_NAME = "_user";
   public static final String NOTE_TABLE_NAME = "_note";
   public static final String USER_ID_COL = "_user_id";
   public static final String USER_NAME_COL = "_name";
   public static final String USER_EMAIL_COL = "_email";
   public static final String USER_PASSWORD_COL = "_password";
   public static final String USER_TABLE_CREATE_SQL = "CREATE TABLE IF NOT EXISTS " + USER_TABLE_NAME +
           "(" +
           USER_ID_COL + " INTEGER PRIMARY KEY" +
           "," + USER_NAME_COL + " CHAR(64)" +
           "," + USER_EMAIL_COL + " EMAIL " +
           "," + USER_PASSWORD_COL + " PASSWORDTYPE " +
           ");";
   /* Note purposely using column types that will have to be converted for Room. */
   public static final String NOTE_ID_COL = "_note_id";
   public static final String NOTE_COL_TIMESTAMP = "_timestamp";
   public static final String NOTE_COL_TEXT = "_text";
   public static final String NOTE_COL_USER_REF = "_user_id_reference";
   public static final String NOTE_TABLE_CREATE_SQL = "CREATE TABLE IF NOT EXISTS " + NOTE_TABLE_NAME +
           "(" +
           NOTE_ID_COL + " INTEGER PRIMARY KEY " +
           "," + NOTE_COL_TIMESTAMP + " MyTimeStampType" +
           "," + NOTE_COL_TEXT + " MEMO " +
           "," + NOTE_COL_USER_REF + " REFERENCE_PARENT REFERENCES " + USER_TABLE_NAME +"(" + USER_ID_COL + ")" +
           ");";

   DBHelper(Context context) {
      super(context,DATABASE_NAME,null,DATABASE_VERSION);
   }

   @Override
   public void onCreate(SQLiteDatabase db) {
      db.execSQL(USER_TABLE_CREATE_SQL);
      db.execSQL(NOTE_TABLE_CREATE_SQL);
   }

   @Override
   public void onUpgrade(SQLiteDatabase db, int i, int i1) {}

   public long insertUser(Long id, String name, String email, String password) {
      ContentValues cv = new ContentValues();
      if (id != null) {
         cv.put(USER_ID_COL,id);
      }
      cv.put(USER_NAME_COL,name);
      cv.put(USER_EMAIL_COL,email);
      cv.put(USER_PASSWORD_COL,password);
      return this.getWritableDatabase().insert(USER_TABLE_NAME,null,cv);
   }
   public long insertUser(String name, String email, String password) {
      return insertUser(null,name,email,password);
   }

   public long insertNote(Long id, String text, long userId) {
      ContentValues cv = new ContentValues();
      if (id != null) {
         cv.put(NOTE_ID_COL,id);
      }
      cv.put(NOTE_COL_TIMESTAMP,System.currentTimeMillis());
      cv.put(NOTE_COL_TEXT,text);
      cv.put(NOTE_COL_USER_REF,userId);
      return this.getWritableDatabase().insert(NOTE_TABLE_NAME,null,cv);
   }
   public long insertNote(String text, long userId) {
      return insertNote(null,text,userId);
   }

}

并使用上述主活动:-

public class MainActivity extends AppCompatActivity {

    DBHelper dbHelper;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        dbHelper = new DBHelper(this);
        addSomeData();
    }

    void addSomeData() {
        long fredid = dbHelper.insertUser("Fred","fred@email.com","fred12345678");
        if (fredid > 0) {
            dbHelper.insertNote("Lorem ipsum dolor sit amet", fredid);
            dbHelper.insertNote("consectetur adipiscing elit", fredid);
            dbHelper.insertNote("sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", fredid);
        }
        long maryid = dbHelper.insertUser(100L,"Mary","mary@coldmail.moc","mary87654321");
        if (maryid > 0) {
            dbHelper.insertNote("Ut enim ad minim veniam", maryid);
            dbHelper.insertNote("quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.", maryid);
            dbHelper.insertNote("Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.", maryid);
            dbHelper.insertNote(999L,"Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", maryid);
        }
    }
}

使用App Inspection可以显示:-

以及:

并且,使用查询:

因此,应用程序显然已经工作并加载了数据。您还可以看到SQLite的类型关联性已经发挥作用。即,根据应用程序检查,已相应地确定了具有可接受但非标准类型的列的列类型。您还可以看到,预定义的值位于模式中(例如,MyTimeStampType在sqlite_master中不是NUMERIC)。

    • 转变**

第一步是根据Room对数据库组件进行编码。

    • 用户**表的@Entity类:-
@Entity(tableName = DBHelper.USER_TABLE_NAME)
class User {
    @PrimaryKey /* MUST ALWAYS HAVE A PRIMARY KEY */
    @ColumnInfo( name = DBHelper.USER_ID_COL) /* Optional */
    Long userId=null;
    @ColumnInfo(name = DBHelper.USER_NAME_COL)
    String userName;
    @ColumnInfo(name = DBHelper.USER_EMAIL_COL)
    String userEmail;
    @ColumnInfo(name = DBHelper.USER_PASSWORD_COL)
    String userPassword;
}
  • 表名和列名来自原始文件,但不是必需的(分别通过@Entity@ColumnInfo注解)。
    • Note**表的@Entity注解类(包括外键):-
@Entity(
        tableName = DBHelper.NOTE_TABLE_NAME,
        foreignKeys = {
                @ForeignKey(
                        entity = User.class,
                        parentColumns = {DBHelper.USER_ID_COL},
                        childColumns = {DBHelper.NOTE_COL_USER_REF}
                )
        }
)
class Note {
    @PrimaryKey
    @ColumnInfo(name = DBHelper.NOTE_ID_COL)
    Long noteId=null;
    @ColumnInfo(name = DBHelper.NOTE_COL_TIMESTAMP)
    long noteTimestamp=System.currentTimeMillis();
    @ColumnInfo(name = DBHelper.NOTE_COL_TEXT)
    String noteText;
    @ColumnInfo(name = DBHelper.NOTE_COL_USER_REF)
    long noteUserRef;
}

@Database注解的抽象类(基本的,用于编译和生成生成的java):-

@Database(entities = {User.class,Note.class}, exportSchema = false, version = DBHelper.DATABASE_VERSION)
abstract class TheDatabase extends RoomDatabase {
}

编译(Ctrl + F9)并:-

Room希望找到的是User和Note表的SQL。
由于原始代码有向每个表中插入行的方法,因此也必须编写等效的方法。因此,@Dao注解接口(为简洁起见,所有这些都在一个接口中):-

@Dao
interface AllDAOs {
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    long insertUser(User user);
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    long insertNote(Note note);
}

为了能够使用这个接口,我们需要修改@Database注解类。为了让演示程序能够真正演示,我们将使用一个单例示例,作为获取单示例的方法。所以:-

@Database(entities = {User.class,Note.class}, exportSchema = false, version = DBHelper.DATABASE_VERSION)
abstract class TheDatabase extends RoomDatabase {
    abstract AllDAOs getAllDAOs();

    private static TheDatabase instance;
    public static TheDatabase getInstance(Context context) {
        if (instance==null) {
            instance= Room.databaseBuilder(context,TheDatabase.class,DBHelper.DATABASE_NAME)
                    .allowMainThreadQueries() /* For brevity of the demo */
                    .build();
        }
        return instance;
    }
}

现在,如果编译(将检查DAO方法),它编译,但有一个警告:-

warning: _user_id_reference column references a foreign key but it is not part of an index. This may trigger full table scans whenever parent table is modified so you are highly advised to create an index that covers this column. - a.a.so75484142javaconverttoroom.Note

对Note类做一点小改动

@ColumnInfo(name = DBHelper.NOTE_COL_USER_REF, index = true)
long noteUserRef;
      • 注意**建议在任何更改之后进行编译,并检查构建日志。这可以简化确定问题原因的过程。

由于用户和注解之间的关系,很可能需要访问用户及其注解。因此,为了能够提取此类对象,需要提取类(未使用@Entity注解,即POJO)。
为了演示其他一些摘录,我们还介绍了其他一些方法。一种方法用于获取所有用户的列表,另一种方法用于根据用户的id获取单个用户。
因此,所有DAO变成:-

@Dao
interface AllDAOs {
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    long insertUser(User user);
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    long insertNote(Note note);

    @Transaction
    @Query("SELECT * FROM " + DBHelper.USER_TABLE_NAME)
    List<UserWithNotes> getAllUsersWithNotes();

    @Query("SELECT * FROM " + DBHelper.USER_TABLE_NAME)
    List<User> getAllUsers();
    @Query("SELECT * FROM " + DBHelper.USER_TABLE_NAME + " WHERE " + DBHelper.USER_ID_COL + "=:userId")
    User getUserByUserId(long userId);

}
      • AND**编译成功,无警告。
  • 注意@Transaction,这是因为Room将构建java,在使用@Relation时检索所有子对象。

在这一阶段,可以考虑转换/迁移。
假设这是一个已经分发并使用的App,App用户的数据需要保留,并且每个用户都是唯一的,假设App中的数据库版本为1(如果不是,则需要分别增加)。
尝试访问数据库时将启动转换(不是在获得TheDatabase类的示例时),即数据库将处于Room的控制之下(有替代方法,如在Room获得控制权之前执行迁移)。
要使用/演示主活动将更改为使用房间。因此:-

public class MainActivity extends AppCompatActivity {

    //DBHelper dbHelper;
    TheDatabase db;
    AllDAOs dao;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //dbHelper = new DBHelper(this);
        //addSomeData();

        db = TheDatabase.getInstance(this);
        dao = db.getAllDAOs();
        /* NOTE Database has not yet been accessed */
        for (UserWithNotes uwn: dao.getAllUsersWithNotes()) {
            StringBuilder sb = new StringBuilder();
            for (Note n: uwn.notes) {
                sb.append("\n\tNote TS=" + n.noteTimestamp + " Text is " + n.noteText);
            }
            Log.d("DBINFO","User is " + uwn.user.userName + " email  is " + uwn.user.userEmail + ". There are " + uwn.notes.size() + " notes. They are");
        }
    }
}

现在,如果尝试运行活动(*请勿执行此操作),则将失败,并显示:-

2023-02-18 09:38:12.741 24921-24921/a.a.so75484142javaconverttoroom E/AndroidRuntime: FATAL EXCEPTION: main
    Process: a.a.so75484142javaconverttoroom, PID: 24921
    java.lang.RuntimeException: Unable to start activity ComponentInfo{a.a.so75484142javaconverttoroom/a.a.so75484142javaconverttoroom.MainActivity}: java.lang.IllegalStateException: Pre-packaged database has an invalid schema: _user(a.a.so75484142javaconverttoroom.User).
     Expected:
    TableInfo{name='_user', columns={_user_id=Column{name='_user_id', type='INTEGER', affinity='3', notNull=false, primaryKeyPosition=1, defaultValue='undefined'}, _email=Column{name='_email', type='TEXT', affinity='2', notNull=false, primaryKeyPosition=0, defaultValue='undefined'}, _name=Column{name='_name', type='TEXT', affinity='2', notNull=false, primaryKeyPosition=0, defaultValue='undefined'}, _password=Column{name='_password', type='TEXT', affinity='2', notNull=false, primaryKeyPosition=0, defaultValue='undefined'}}, foreignKeys=[], indices=[]}
     Found:
    TableInfo{name='_user', columns={_user_id=Column{name='_user_id', type='INTEGER', affinity='3', notNull=false, primaryKeyPosition=1, defaultValue='undefined'}, _name=Column{name='_name', type='CHAR(64)', affinity='2', notNull=false, primaryKeyPosition=0, defaultValue='undefined'}, _email=Column{name='_email', type='EMAIL', affinity='1', notNull=false, primaryKeyPosition=0, defaultValue='undefined'}, _password=Column{name='_password', type='PASSWORDTYPE', affinity='1', notNull=false, primaryKeyPosition=0, defaultValue='undefined'}}, foreignKeys=[], indices=[]}

可以确定的是,该房间找到了数据库,但不是预期的数据库,但没有迹象表明预期会进行迁移。
因此可以使用public static final int DATABASE_VERSION = 2;
现在,如果运行(请勿执行此操作),则失败为:-

java.lang.IllegalStateException: A migration from 1 to 2 was required but not found. Please provide the necessary Migration path via RoomDatabase.Builder.addMigration(Migration ...) or allow for destructive migrations via one of the RoomDatabase.Builder.fallbackToDestructiveMigration* methods.

即,Room再次找到数据库,但看到它是版本1,但应该是版本2,并使用迁移。
因此,可以添加一个迁移来相应地基本更改表。然而,如前所述,破译需要更改的内容(根据第一次失败)并不是最容易的任务。因此,为了简化工作,使用生成的java中的SQL。
因此,TheDatabase类现在可以是:-

@Database(entities = {User.class,Note.class}, exportSchema = false, version = DBHelper.DATABASE_VERSION)
abstract class TheDatabase extends RoomDatabase {
    abstract AllDAOs getAllDAOs();

    private static TheDatabase instance;
    public static TheDatabase getInstance(Context context) {
        if (instance==null) {
            instance= Room.databaseBuilder(context,TheDatabase.class,DBHelper.DATABASE_NAME)
                    .allowMainThreadQueries() /* For brevity of the demo */
                    .addMigrations(migratedFromVersion1toVersion2)
                    .build();
        }
        return instance;
    }

    static Migration migratedFromVersion1toVersion2 = new Migration(1,2) {
        @Override
        public  void migrate(@NonNull SupportSQLiteDatabase db) {
            String originalSuffix = "_original";
            db.execSQL("ALTER TABLE " + DBHelper.USER_TABLE_NAME + " RENAME TO " + DBHelper.USER_TABLE_NAME+originalSuffix);
            db.execSQL("ALTER TABLE " + DBHelper.NOTE_TABLE_NAME + " RENAME TO " + DBHelper.NOTE_TABLE_NAME+originalSuffix);
            /* SQL below copied from the generated java */
            db.execSQL("CREATE TABLE IF NOT EXISTS `_user` (`_user_id` INTEGER, `_name` TEXT, `_email` TEXT, `_password` TEXT, PRIMARY KEY(`_user_id`))");
            db.execSQL("CREATE TABLE IF NOT EXISTS `_note` (`_note_id` INTEGER, `_timestamp` INTEGER NOT NULL, `_text` TEXT, `_user_id_reference` INTEGER NOT NULL, PRIMARY KEY(`_note_id`), FOREIGN KEY(`_user_id_reference`) REFERENCES `_user`(`_user_id`) ON UPDATE NO ACTION ON DELETE NO ACTION )");

            /* Now to get the original data */
            db.execSQL("INSERT INTO " + DBHelper.USER_TABLE_NAME + " SELECT * FROM " + DBHelper.USER_TABLE_NAME+originalSuffix);
            db.execSQL("INSERT INTO " + DBHelper.NOTE_TABLE_NAME + " SELECT * FROM " + DBHelper.NOTE_TABLE_NAME+originalSuffix);

            /* Create the additional Index (done after the inserts as more efficient) */
            db.execSQL("CREATE INDEX IF NOT EXISTS `index__note__user_id_reference` ON `_note` (`_user_id_reference`)");

            /* Cleanup */
            db.execSQL("DROP TABLE IF EXISTS " + DBHelper.NOTE_TABLE_NAME+originalSuffix);
            db.execSQL("DROP TABLE IF EXISTS " + DBHelper.USER_TABLE_NAME+originalSuffix);
        }
    };
}

运行时,日志包括:-

2023-02-18 10:17:20.003 26161-26161/a.a.so75484142javaconverttoroom D/DBINFO: User is Fred email  is fred@email.com. There are 3 notes. They are
        Note TS=1676667551898 Text is Lorem ipsum dolor sit amet
        Note TS=1676667551898 Text is consectetur adipiscing elit
        Note TS=1676667551899 Text is sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
2023-02-18 10:17:20.004 26161-26161/a.a.so75484142javaconverttoroom D/DBINFO: User is Mary email  is mary@coldmail.moc. There are 4 notes. They are
        Note TS=1676667551900 Text is Ut enim ad minim veniam
        Note TS=1676667551900 Text is quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
        Note TS=1676667551901 Text is Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
        Note TS=1676667551901 Text is Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

*即,数据库现在显然可用于(已转换)Room并保留原始数据

相关问题