将数据从 sqlite 数据库文件移动到房间

Move data from sqlite database file to room

我有一个处理外部数据库(sqlite数据库文件)的项目,我想移动到房间数据库如下

  1. 数据库 (student_database.db) 的内容最初由第三方应用程序输入,例如(DB Browser for SQLite)。
  2. 当我 运行 我的应用程序创建了数据库空间并将数据从 student_database.db 3 我的应用程序在 Recycler View 中显示来自 (room db) 的数据
  3. 新数据可以添加到数据库(房间)

这是我项目的一些文件

 @Entity(tableName = "books_table")
public class Book {

 @PrimaryKey(autoGenerate = true)
 @ColumnInfo(name = "book_id")
 private long id;

 @ColumnInfo(name = "book_name")
 private String name;

 @ColumnInfo(name = "all")
 private long all;

 @ColumnInfo(name = "profile_id")
 private long profileId;

 public Book() {
 }

 public Book(String name, long all, long profileId) {
     this.name = name;
     this.all = all;
     this.profileId = profileId;
 }


@Dao
public interface BookDAO {

 @Insert
 void insertBook(Book... book);

 @Update
 void updateBook(Book... book);


@Entity (tableName = "profiles_table")
public class Profile {

    @PrimaryKey(autoGenerate = true)
    long id;

    @ColumnInfo (name = "profile_name")
    String profileName;

    public Profile() {
    }

    public Profile(long id, String profileName) {
        this.id = id;
        this.profileName = profileName;
    }

    public Profile(String profileName) {
        this.profileName = profileName;
    }

@Dao
public interface ProfileDAO {

    @Insert
    void insertProfile(Profile... profile);

    @Update
    void updateProfile(Profile... profile);


@Database(entities = {Book.class  , Profile.class}, version = 1, exportSchema = false)
public abstract class MyRoomDatabase extends RoomDatabase {

    public abstract BookDAO bookDao();
    public abstract ProfileDAO profileDAO();

    private static volatile MyRoomDatabase INSTANCE;

    private static final int NUMBER_OF_THREADS = 4;

    static final ExecutorService databaseWriteExecutor =
            Executors.newFixedThreadPool(NUMBER_OF_THREADS);



    static MyRoomDatabase getDatabase(final Context context) {
        if (INSTANCE == null) {
            synchronized (MyRoomDatabase.class) {
                if (INSTANCE == null) {
                    INSTANCE = Room.databaseBuilder(context.getApplicationContext(),
                            MyRoomDatabase.class, "student_prepare_room_database")
                            .addCallback(sRoomDatabaseCallback)
                            .build();
                }
            }
        }
        return INSTANCE;
    }
    

    private static RoomDatabase.Callback sRoomDatabaseCallback = new RoomDatabase.Callback() {
        @Override
        public void onCreate(@NonNull SupportSQLiteDatabase db) {
            super.onCreate(db);

            // If you want to keep data through app restarts,
            // comment out the following block
            databaseWriteExecutor.execute(() -> {
                // Populate the database in the background.
                // If you want to start with more words, just add them.

            });
        }
    };
}

student_database.db

CREATE TABLE "profile_table" ("id" INTEGER,"profile_name" TEXT,PRIMARY KEY("id")); [1]: https://i.stack.imgur.com/lDhtR.png

CREATE TABLE "books_table" ("book_id" INTEGER,"book_name" TEXT,"all" INTEGER,"profile_id" INTEGER,PRIMARY KEY("book_id" AUTOINCREMENT)); [2]: https://i.stack.imgur.com/NiuZe.png

通常,这将通过使外部数据库成为资产(预打包的数据库)并利用 .createFromAsset 来进行,后者将进行数据库的复制。

但是,使用上面的 REQUIRE 资产数据库严格符合由 classes 定义的房间模式 @Entity并包含在通过 @Database 注释提供的实体 list/array 中。

可以使用prePackagedDatabaseCallback修改预打包的数据库来适配。但是,对外部数据库进行更改以使其符合房间的预期可能更简单。

在您的情况下,例如,您在 配置文件 table 中不匹配,因为在 external/pre-packaged 数据库中您有 id 作为(有效地)INTEGER PRIMARY KEY 而通过使用 @PrimaryKey(autoGenerate = true) 房间期望 INTEGER PRIMARY KEY AUTOINCREMENT.

  • AUTOINCREMENT(在房间 autogenerate = true 中)效率低下且很少需要。您可以改为使用 @PrimaryKey.

让房间变得简单

只讨论了一个不匹配的例子,因为很可能没有必要深入研究所有会导致问题的不匹配。当用 @Entity 注释的 classes 已经创建并包含在 classes 的列表中时,当您编译项目时,空间将生成预期的 CREATE TABLE .... 语句@Database 注释。因此,您可以根据 room.

创建的 SQL 轻松地对 external/pre-packaged table 进行必要的更改。

Room创建的SQL在java(generated)中找到(在Android View)m,要查找的 class 与 class 同名,注释为 @Database 但后缀为 _Impl。 SQL 在名为 createAllTables 的方法中找到。您不想创建 room_master_table,它是一个 table,room 用于存储模式的哈希表示,并用于检查是否已对模式进行更改。

演示

根据您的问题使用 code/schemas(进行一些细微的更改以简化演示)。

第 1 步 - 根据您的模式预打包数据库

  • 显然你有这个

首先,根据您的 SQL 创建了一个数据库,并在第三方工具 (Navicat) 中填充了大约 100 个配置文件和 10000 本书:-

DROP TABLE IF EXISTS "profile_table";
DROP TABLE IF EXISTS "books_table";
CREATE TABLE IF NOT EXISTS "profile_table" ( "id" INTEGER, "profile_name" TEXT, PRIMARY KEY("id") );
CREATE TABLE "books_table" ( "book_id" INTEGER, "book_name" TEXT, "all" INTEGER, "profile_id" INTEGER, PRIMARY KEY("book_id" AUTOINCREMENT) );
/* Generate some data 100 profiles and 10000 books*/
WITH loop(x) AS (SELECT 1 UNION ALL SELECT x+1 FROM loop LIMIT 100)
INSERT INTO profile_table (profile_name) SELECT  'Profile'||x FROM loop;
WITH loop(x) AS (SELECT 1 UNION ALL SELECT x+1 FROM loop LIMIT 10000)
INSERT INTO books_table (book_name,`all`,profile_id) SELECT 'Book'||x,abs(random() % 10),(abs(random()) % (SELECT max(id) FROM profile_table)) + 1 FROM loop;
/* Display the data */
SELECT * FROM books_table JOIN profile_table ON profile_id=id ORDER BY CAST(substr(profile_name,8) AS INTEGER);

查询显示数据如:-

  • 实际显示了10000行

步骤 2 - 创建资产 资产目录已创建,数据库已复制并粘贴为 student_database.db :-

  • 请注意,这是为了重新运行能力
  • 制作的副本

步骤 3 添加 createFromAsset 和对 运行 初始测试的其他更改

MyRoomDatabase class 已更改为添加 .createFromAsset :-

            if (INSTANCE == null) {
                INSTANCE = Room.databaseBuilder(context.getApplicationContext(),
                        MyRoomDatabase.class, "student_prepare_room_database")
                        .createFromAsset("student_database.db") //<<<<< STEP 3 - ADDED
                        .addCallback(sRoomDatabaseCallback) //<<<< added for convenience/brevity
                        .allowMainThreadQueries()
                        .build();
            }

另外 BookDAO 通过添加进行了修改:-

@Query("SELECT * FROM books_table")
List<Book> getAllBooks();
  • 允许简单访问数据库(除非访问数据库不会打开检查等)。

最后 MainActivity 被编码为:-

public class MainActivity extends AppCompatActivity {

    MyRoomDatabase db;
    BookDAO bookDAO;
    ProfileDAO profileDAO;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        db = MyRoomDatabase.getDatabase(this);
        bookDAO = db.bookDao();
        profileDAO = db.profileDAO();
        for (Book b: bookDAO.getAllBooks()) {
            Log.d("BOOKINFO","Book is " + b.getName());
        }
    }
}

第 4 步房间 SQL

项目随后被编译(getters/setters 添加)createALLTables 为:-

  • 注意配置文件上的 AUTOINCREMENT table。

步骤 5 运行(见注释)

  • 注意这一步纯粹是为了演示如果数据库没有改变会发生什么。

综上所述,如果应用程序是 运行(不是应该的,因为它会失败)。正如预期的那样,它因 Expected/Found 不匹配问题而失败,但很明显它复制了资产:-

2022-01-05 07:59:02.200 1941-1941/? D/AndroidRuntime: Shutting down VM
2022-01-05 07:59:02.202 1941-1941/? E/AndroidRuntime: FATAL EXCEPTION: main
    Process: a.a.so70580333prepackageddatabase, PID: 1941
    java.lang.RuntimeException: Unable to start activity ComponentInfo{a.a.so70580333prepackageddatabase/a.a.so70580333prepackageddatabase.MainActivity}: java.lang.IllegalStateException: Pre-packaged database has an invalid schema: books_table(a.a.so70580333prepackageddatabase.Book).
     Expected:
    TableInfo{name='books_table', columns={all=Column{name='all', type='INTEGER', affinity='3', notNull=true, primaryKeyPosition=0, defaultValue='null'}, book_id=Column{name='book_id', type='INTEGER', affinity='3', notNull=true, primaryKeyPosition=1, defaultValue='null'}, book_name=Column{name='book_name', type='TEXT', affinity='2', notNull=false, primaryKeyPosition=0, defaultValue='null'}, profile_id=Column{name='profile_id', type='INTEGER', affinity='3', notNull=true, primaryKeyPosition=0, defaultValue='null'}}, foreignKeys=[], indices=[]}
     Found:
    TableInfo{name='books_table', columns={all=Column{name='all', type='INTEGER', affinity='3', notNull=false, primaryKeyPosition=0, defaultValue='null'}, profile_id=Column{name='profile_id', type='INTEGER', affinity='3', notNull=false, primaryKeyPosition=0, defaultValue='null'}, book_id=Column{name='book_id', type='INTEGER', affinity='3', notNull=false, primaryKeyPosition=1, defaultValue='null'}, book_name=Column{name='book_name', type='TEXT', affinity='2', notNull=false, primaryKeyPosition=0, defaultValue='null'}}, foreignKeys=[], indices=[]}
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2913)

STEP 6 修改预打包数据库

重新访问第 3 方工具并使用以下内容:-

DROP TABLE IF EXISTS books_table_original;
DROP TABLE IF EXISTS profile_table_original;
ALTER TABLE books_table RENAME TO books_table_original;
ALTER TABLE profile_table RENAME TO profile_table_original;
/* COPIED FROM createAllTables method */
CREATE TABLE IF NOT EXISTS `books_table` (`book_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `book_name` TEXT, `all` INTEGER NOT NULL, `profile_id` INTEGER NOT NULL);
CREATE TABLE IF NOT EXISTS `profiles_table` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `profile_name` TEXT);
INSERT INTO profiles_table SELECT * FROM profile_table_original;
INSERT INTO books_table SELECT * FROM books_table_original;
/* CLEANUP */
DROP TABLE IF EXISTS books_table_original;
DROP TABLE IF EXISTS profile_table_original;
VACUUM;
  • 请注意 expected/found 问题甚至都没有被关注

    • 如果查看它们,您会发现 All 列应该为 NOT NULL,但它发现 All 列没有定义,因此允许 NULLS(不匹配)。
    • 同样适用于 book_id 列(另一个不匹配)
    • 同样适用于 profile_id 列(另一个不匹配)
    • 如果这些是固定的,那么 profiles_table 将导致更多不匹配,因为 table 名称不同(altered/corrected 在以上)。
  • 请注意,如果引入了外键约束,那么删除 table 的顺序与创建它们的顺序一样重要(但我相信空间排序 tables)。

  • 以上假设预打包数据库中的table列顺序与Room构建的SQL中的列顺序相同。如果顺序不同,则可以将 INSERT 修改为:-

    • 指定要按选择顺序插入的列,例如INSERT INTO profiles_table (id,profile_name) SELECT * FROM profile_table_original

    • 根据定义的顺序指定 SELECT 列,例如INSERT INTO profiles_table SELECT (id,profile_name) FROM profile_table_original

    • 为 INSERT 和 SELECT 指定列而不是 *,例如INSERT INTO profiles_table (profile_name,id) SELECT (profile_name,id) FROM profile_table_original

      • 请注意,最后一个示例有意使用不同的列顺序来演示不同的顺序。它不会影响 table 中列的顺序,只会影响 SQLite.
      • 中构建的中间输出中列的顺序

运行完成上述操作后,数据库被保存并复制到资产文件夹。

  • 在这种情况下,数据库最初被粘贴为 AlteredFroRoom_student_database.db,student_database.db 屁股被删除,然后 AlterForRoom_student_database.db 被复制并粘贴为 student_database.db。 注意 对于要分发的应用程序,您不希望拥有额外的数据库副本,因为这会增加分发的大小。

然后卸载该应用程序,然后重新运行并成功运行。

  • 请注意,由于已复制数据库,不会调用 onCreate 回调(会调用 onOpen)。

日志包括:-

2022-01-05 08:56:15.975 D/BOOKINFO: Book is Book1
2022-01-05 08:56:15.975 D/BOOKINFO: Book is Book2
2022-01-05 08:56:15.975 D/BOOKINFO: Book is Book3
2022-01-05 08:56:15.975 D/BOOKINFO: Book is Book4
2022-01-05 08:56:15.975 D/BOOKINFO: Book is Book5
2022-01-05 08:56:15.975 D/BOOKINFO: Book is Book6
2022-01-05 08:56:15.976 D/BOOKINFO: Book is Book7
2022-01-05 08:56:15.976 D/BOOKINFO: Book is Book8
2022-01-05 08:56:15.976 D/BOOKINFO: Book is Book9
2022-01-05 08:56:15.976 D/BOOKINFO: Book is Book10
2022-01-05 08:56:15.976 D/BOOKINFO: Book is Book11
2022-01-05 08:56:15.976 D/BOOKINFO: Book is Book12
2022-01-05 08:56:15.976 D/BOOKINFO: Book is Book13
2022-01-05 08:56:15.976 D/BOOKINFO: Book is Book14
2022-01-05 08:56:15.976 D/BOOKINFO: Book is Book15
2022-01-05 08:56:15.976 D/BOOKINFO: Book is Book16
....