【问题标题】:Move data from sqlite database file to room将数据从 sqlite 数据库文件移动到房间
【发布时间】:2022-01-04 14:29:40
【问题描述】:

我有一个处理外部数据库(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

【问题讨论】:

    标签: java android database sqlite android-room


    【解决方案1】:

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

    但是,使用上述方法确实要求资产数据库严格符合房间模式,该模式由带有@Entity 注释的类定义,并包含在通过提供的实体列表/数组中@Database注解。

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

    在您的情况下,例如,您在 profile 表中存在不匹配,因为在外部/预打包数据库中您的 id 为(有效) INTEGER PRIMARY KEY 同时使用 @PrimaryKey(autoGenerate = true) 房间期望 INTEGER PRIMARY KEY AUTOINCREMENT

    • AUTOINCREMENT(在房间autogenerate = true)效率低下,很少需要。你可以改用@PrimaryKey

    让房间变得简单

    只讨论了一个不匹配的例子,因为几乎没有必要深入研究会导致问题的所有不匹配。也就是说,当您编译项目时,当使用@Entity 注释的类已创建并包含在@Database 注释的类列表中时,房间将生成预期的CREATE TABLE .... 语句。因此,您可以根据 room 创建的 SQL 轻松地对外部/预打包的表进行所需的更改。

    Room创建的SQL在java(generated)(Android View中可见)m中找到,要查找的类是同名的使用@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);
    

    显示数据的查询,例如:-

    • 实际上显示了 10000 行

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

    • 请注意,此副本是为了可重新运行而制作的

    第 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());
            }
        }
    }
    

    第 4 步 Room SQL

    然后编译项目(添加了 getter/setter)createALLTables 是:-

    • 请注意配置文件表上的 AUTOINCREMENT。

    第 5 步运行(见注)

    • 注意此步骤纯粹是为了演示如果不更改数据库会发生什么。

    综上所述,如果应用程序随后运行(不应该是它会失败)。正如预期的那样,它因预期/发现不匹配问题而失败,但显然它已经复制了资产:-

    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 步修改预打包的数据库

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

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

      • 如果查看它们,您可以看到 All 列应为 NOT NULL,但它发现 All 列没有定义,因此允许 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 中构建的中间输出中列的顺序。

    以上运行后,数据库保存并复制到assets文件夹中。

    • 在这种情况下,数据库最初被粘贴为 AlteredFroRoom_student_database.db,student_database.db ass 被删除,然后 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中的列顺序呢(一次是 column(all) ,但第二个是 book_id "Expected" ,type_id on"Found")
    • @bmsbms16 顺序是 Room 报告的方式,而不是列的顺序。此外,步骤 5 是一个示例,说明当/如果不进行更改时会发生什么,它不是应该遵循的步骤。我将编辑答案以表明第 5 步仅用于演示如果不遵循第 6 步将导致的错误。
    猜你喜欢
    • 2022-08-02
    • 1970-01-01
    • 2019-02-13
    • 1970-01-01
    • 2018-06-16
    • 1970-01-01
    • 2021-08-13
    • 1970-01-01
    • 2011-02-14
    相关资源
    最近更新 更多