将数据从sqlite数据库文件转移到room

3

我有一个涉及外部数据库(sqlite数据库文件)的项目,我想按以下方式转移到room数据库:

  1. 首先由第三方应用程序(例如SQLite的DB Browser)输入数据库(student_database.db)的内容。
  2. 当我运行我的应用程序时,将创建room数据库并将数据从student_database.db中导入。
  3. 我的应用程序在Recycler View中显示(room db)的数据。
  4. 可以向数据库(room)中添加新数据。

这是我的项目中的一些文件。

 @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.

            });
        }
    };
}

学生数据库.db

CREATE TABLE "profile_table" ("id" INTEGER,"profile_name" TEXT,PRIMARY KEY("id")); [1]: https://istack.dev59.com/lDhtR.webp

CREATE TABLE "books_table" ("book_id" INTEGER,"book_name" TEXT,"all" INTEGER,"profile_id" INTEGER,PRIMARY KEY("book_id" AUTOINCREMENT)); [2]: https://istack.dev59.com/NiuZe.webp

1个回答

2
通常,这可以通过将外部数据库作为资产(预打包数据库)并利用 .createFromAsset 来进行,该方法将执行数据库的复制。
然而,使用上述方法需要资产数据库严格符合通过 @Entity 注释的类定义的 room 模式,并包含在通过 @Database 注释提供的实体列表/数组中。
可以使用 prePackagedDatabaseCallback 修改预打包的数据库以适应。但是,更简单的方法可能是对外部数据库进行更改,使其与 room 期望的相匹配。
例如,在您的情况下,外部/预打包的数据库中的 profile 表存在不匹配,因为您将 id 设置为(有效地)INTEGER PRIMARY KEY,而使用 @PrimaryKey(autoGenerate = true),room 期望 INTEGER PRIMARY KEY AUTOINCREMENT
  • AUTOINCREMENT(在 room 中 autogenerate = true)效率低下且很少需要。您可以更改为使用 @PrimaryKey
让 Room 让它变得简单
只讨论了一个不匹配的示例,因为很可能没有必要深入研究所有会导致问题的不匹配。当创建并包含在 @Database 注释的类列表中的带有 @Entity 注释的类时,编译项目时 room 将生成预期的 CREATE TABLE .... 语句。因此,您可以根据 Room 创建的 SQL 轻松地对外部/预打包的表进行所需的更改。
Room 创建的 SQL 可在 java(生成)中找到(在 Android View 中可见),要查找的类与用 @Database 注释注释的类同名,但后缀为 _Impl。 SQL 可在名为 createAllTables 的方法中找到。您不想创建 room_master_table,这是 room 用于存储模式的哈希表示形式并用于检查是否已对模式进行更改的表。
演示
使用与您的问题相同的代码/模式(通过一些微小的更改简化演示)。
第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);

展示数据的查询如下:

  • enter image description here
  • 实际上显示了10000行

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

enter image description here

  • 注意,已经复制了一份以便重新运行。

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

MyRoomDatabase类已更改以添加.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());
        }
    }
}

第四步 房间SQL

项目随后被编译(添加了getter/setter),createALLTables如下:

enter image description here

  • 请注意profile表上的AUTOINCREMENT。

第五步 运行(见注释)

  • 注释:这一步仅是为了演示如果数据库未被修改会发生什么。

以上所有内容,如果应用程序运行(尽管不应该运行,因为它会失败),则会像预期的那样失败,并出现“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)

步骤6 修改预打包数据库

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

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;
  • 请注意,预期/发现的问题甚至没有被查看

    • 如果已经查看,您可以看到全部列应为NOT NULL,但它发现全部列未定义,因此允许NULLS(不匹配)。
    • 对于book_id列也是如此(另一个不匹配)
    • 对于profile_id列也是如此(另一个不匹配)
    • 如果这些问题得到解决,则profiles_table将会出现更多的不匹配,因为表名不同(在上面进行了更改/纠正)。
  • 请注意,如果引入外键约束,则删除表的顺序和创建表的顺序都很重要(但我认为Room会相应地排序表)。

  • 以上假设预打包数据库中的表与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

      • 请注意,此最后一个示例故意使用了不同的列顺序以演示不同的顺序。它不会影响任何表中列的顺序,只会影响在SQLite中构建的中间输出中列的顺序。

运行以上操作后,将保存数据库并将其复制到资产文件夹。

  • enter image description here
  • 在这种情况下,数据库最初被粘贴为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
....

谢谢您的回答,但是在STEP5中列的顺序怎么样呢?第一列是所有列,但第二列是book_id“Expected”,type_id为“Found”。 - bmsbms16
@bmsbms16,房间报告的顺序并不是列的顺序。此外,步骤5是如果未进行修改会发生什么的示例,而不是应该遵循的步骤。我将编辑答案以表明步骤5仅用于演示如果未按照步骤6进行操作会导致的错误。 - MikeT
@MikeT 谢谢。但我的问题还没有解决。数据库里充满了数据,但我的应用程序没有显示任何内容。我的意思是我用DB浏览器填充的外部数据库。你能联系我吗? - Sana Ebadi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接