【问题标题】:How do I resolve a Room invalid schema error involving 'notNull' and 'primaryKeyPosition' on prepopulated database?如何解决在预填充数据库上涉及“notNull”和“primaryKeyPosition”的 Room 无效架构错误?
【发布时间】:2020-01-15 17:34:23
【问题描述】:

我的问题: 我正在转换现有的 Java 桌面应用程序,并且正在学习 Android Room 开发,并通过 SQLite Studio 创建了一个预填充的 SQLite 数据库,其中包含一个多对多连接表(即 Author_By_Source)。此数据库和表在现有 Java 桌面应用程序中使用。我正在尝试解决 'notNull' 和 'primaryKeyPosition' 属性之间的无效架构不匹配问题。我无法让这两个属性更新。这是我试图为 AuthorID 字段解决的错误片段,但它也是同一错误消息中 SourceID 的相同差异。所以两个字段需要相同的分辨率:

    Expected:
    TableInfo{name='Author_By_Source', columns={AuthorID=Column{name='AuthorID', type='INTEGER', affinity='3', notNull=true, primaryKeyPosition=1, defaultValue='null'}, ...
    Found:
    TableInfo{name='Author_By_Source', columns={AuthorID=Column{name='AuthorID', type='INTEGER', affinity='3', notNull=false, primaryKeyPosition=0, defaultValue='null'}, ...

我的尝试: 我尝试使用 SQLite Studio 界面来构建和构建数据库,并编辑了“Author_By_Source”表的表结构。我将每个字段设置为 Not Null,但当我重新运行应用程序时,它不会反映为“真”。我还没有确定如何指定primaryKeyPosition。我通过更彻底地阅读房间注释的选项和方法来尝试相反的方法,但我没有成功。在我的整个项目中,我一直在阅读一些 Room 文档,并访问了一些论坛和教程,但没有找到任何解决这个问题的具体内容。

我正在尝试做的事情: 我正在尝试使用具有多个表的预填充数据库,其中一些表具有用于多对多关系的连接表。我希望有一些方向。

以下是我如何注释我的 AuthorBySource 类,这可能会有所帮助:

 @Entity(tableName = "Author_By_Source", primaryKeys = {"AuthorID", "SourceID"},  foreignKeys = {
        @ForeignKey(entity = Authors.class, parentColumns = "AuthorID", childColumns = "AuthorID"),
        @ForeignKey(entity = Sources.class, parentColumns = "SourceID", childColumns = "SourceID")},
        indices = {@Index("AuthorID"), @Index("SourceID")})
public class AuthorBySource {
    @ColumnInfo(name = "AuthorID")
    private int authorID;
    @ColumnInfo(name = "SourceID")
    private int sourceID;

【问题讨论】:

    标签: java android sqlite android-room


    【解决方案1】:

    您是否在每次进行更改时重新复制资产文件并卸载应用程序?如果没有,这并不能解决问题,那么:-

    如果您编译房间代码,然后查看 your_database_class_impl 中的 java(generated)(其中 your_database_class 是您的 @Database 类名)在 createAllTables 方法中。这有用于创建数据库的 SQL(您可以忽略 room_master SQL)。

    例如(这基于您的实体和最小引用表):-

      @Override
      public void createAllTables(SupportSQLiteDatabase _db) {
        _db.execSQL("CREATE TABLE IF NOT EXISTS `Authors` (`AuthorID` INTEGER, `AuthorName` TEXT, PRIMARY KEY(`AuthorID`))");
        _db.execSQL("CREATE TABLE IF NOT EXISTS `Sources` (`SourceID` INTEGER, `SourceName` TEXT, PRIMARY KEY(`SourceID`))");
        _db.execSQL("CREATE TABLE IF NOT EXISTS `Author_By_Source` (`AuthorID` INTEGER NOT NULL, `SourceID` INTEGER NOT NULL, PRIMARY KEY(`AuthorID`, `SourceID`), FOREIGN KEY(`AuthorID`) REFERENCES `Authors`(`AuthorID`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`SourceID`) REFERENCES `Sources`(`SourceID`) ON UPDATE NO ACTION ON DELETE NO ACTION )");
        _db.execSQL("CREATE INDEX IF NOT EXISTS `index_Author_By_Source_AuthorID` ON `Author_By_Source` (`AuthorID`)");
        _db.execSQL("CREATE INDEX IF NOT EXISTS `index_Author_By_Source_SourceID` ON `Author_By_Source` (`SourceID`)");
        _db.execSQL("CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)");
        _db.execSQL("INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '953f8512db886fb7d206fa561a7117c4')");
      }
    

    您需要相应地转换数据库,即按照定义,因此例如您可以在 SQLite Studio 中针对上述情况使用

    /* Just once */
    PRAGMA foreign_keys = 0;
    
    /* Per table */
    DROP TABLE IF EXISTS Author_By_Source_new;
    DROP TABLE IF EXISTS Author_By_Source_original;
    CREATE TABLE IF NOT EXISTS Author_By_Source_new copy_generated_sql_for_the_column_definitions_etc;
    DELETE FROM Author_By_Source_new;
    INSERT INTO Author_By_Source_new SELECT * FROM Author_By_Source;
    ALTER TABLE Author_By_Source RENAME TO Author_By_Source_original;
    ALTER TABLE Author_By_Source_new RENAME TO Author_By_Source;
    /* IF HAPPY THEN DO */
    -- DROP TABLE IF EXISTS Author_By_Source_original; /* NOTE TURNED OFF */
    
    ...... do the equivalent for all tables
    
    
    /* Just once */
    PRAGMA foreign_keys = 1;
    PRAGMA foreign_key_check;
    PRAGMA integrity_check; 
    
    • copy_generated_sql_for_the_column_definitions_etc 应相应地替换为生成的 SQL 中的列定义部分(或复制整个语句并更改表名)。
    • 注意 INSERT INTO .... 语句假定新表和原始表的列顺序相同,否则使用 INSERT INTO (column_list).... 列按照生成的 SQL 例如INSERT INTO Author_By_Source_new (AuthorID,SourceID) SELECT * FROM Author_By_Source;
    • CREATE TABLE SQL 将是(基于上面生成的 SQL)

      • CREATE TABLE IF NOT EXISTS `Author_By_Source_new` (`AuthorID` INTEGER NOT NULL, `SourceID` INTEGER NOT NULL, PRIMARY KEY(`AuthorID`, `SourceID`), FOREIGN KEY(`AuthorID`) REFERENCES `Authors`(`AuthorID`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`SourceID`) REFERENCES `Sources`(`SourceID`) ON UPDATE NO ACTION ON DELETE NO ACTION );
      • 注意 表名已更改,即以 _new 为后缀

    示例

    这是一个基于您的代码可用内容的示例。

    原始预填充数据库

    使用 SQLite 管理工具 (Navicat) 生成数据库:-

    DROP TABLE IF EXISTS Author_By_Source;
    DROP TABLE IF EXISTS Authors;
    DROP TABLE IF EXISTS Sources;
    CREATE TABLE IF NOT EXISTS Authors (AuthorID INTEGER PRIMARY KEY, AuthorName TEXT);
    CREATE TABLE IF NOT EXISTS Sources (SourceID INTEGER PRIMARY KEY, SourceName TEXT);
    /* Create the table so that it would cause issues in Room  */
    CREATE TABLE IF NOT EXISTS Author_By_Source (
        AuthorId INTEGER, SourceId INTEGER, 
        FOREIGN KEY (AuthorID) REFERENCES Authors(AuthorID), 
        FOREIGN KEY (SourceID) REFERENCES Sources(SourceID)
    );
    
    INSERT INTO Authors (AuthorName) VALUES ('Fred'),('Mary'),('Joan'),('Bert'),('Alan');
    INSERT INTO Sources (SourceName) VALUES ('S1'),('S2'),('S3'),('S4'),('S5');
    INSERT INTO Author_By_Source VALUES(1,1),(2,2),(3,3),(4,4),(5,5),(1,4),(3,4),(5,2);
    
    SELECT AuthorName, SourceName 
        FROM Author_By_Source 
            JOIN Authors ON Author_By_Source.AuthorID = Authors.AuthorID
            JOIN Sources ON Author_By_Source.SourceID = Sources.SourceID
    ;
    

    运行后:-

    AuthorName  SourceName
    Fred        S1
    Mary        S2
    Joan        S3
    Bert        S4
    Alan        S5
    Fred        S4
    Joan        S4
    Alan        S2
    

    正如预期的那样,尝试使用它会导致:-

    java.lang.RuntimeException: Unable to start activity ComponentInfo{a.a.so59756782javaroomprepopulatedconversion/a.a.so59756782javaroomprepopulatedconversion.MainActivity}: java.lang.IllegalStateException: Pre-packaged database has an invalid schema: Authors(a.a.so59756782javaroomprepopulatedconversion.Authors).
     Expected:
    TableInfo{name='Authors', columns={AuthorName=Column{name='AuthorName', type='TEXT', affinity='2', notNull=false, primaryKeyPosition=0, defaultValue='null'}, AuthorID=Column{name='AuthorID', type='INTEGER', affinity='3', notNull=true, primaryKeyPosition=1, defaultValue='null'}}, foreignKeys=[], indices=[]}
     Found:
    TableInfo{name='Authors', columns={AuthorID=Column{name='AuthorID', type='INTEGER', affinity='3', notNull=false, primaryKeyPosition=1, defaultValue='null'}, AuthorName=Column{name='AuthorName', type='TEXT', affinity='2', notNull=false, primaryKeyPosition=0, defaultValue='null'}}, foreignKeys=[], indices=[]}
    

    转换

    根据答案,以下内容用于转换数据库:-

    BEGIN TRANSACTION;
    DROP TABLE IF EXISTS Author_By_Source;
    DROP TABLE IF EXISTS Authors;
    DROP TABLE IF EXISTS Sources;
    CREATE TABLE IF NOT EXISTS `Authors` (`AuthorID` INTEGER NOT NULL, `AuthorName` TEXT, PRIMARY KEY(`AuthorID`));
    CREATE TABLE IF NOT EXISTS `Sources` (`SourceID` INTEGER NOT NULL, `SourceName` TEXT, PRIMARY KEY(`SourceID`));
    /* Create the table so that it would cause issues in Room  */
    CREATE TABLE IF NOT EXISTS Author_By_Source (
        AuthorId INTEGER, SourceId INTEGER, 
        FOREIGN KEY (AuthorID) REFERENCES Authors(AuthorID), 
        FOREIGN KEY (SourceID) REFERENCES Sources(SourceID)
    );
    
    INSERT INTO Authors (AuthorName) VALUES ('Fred'),('Mary'),('Joan'),('Bert'),('Alan');
    INSERT INTO Sources (SourceName) VALUES ('S1'),('S2'),('S3'),('S4'),('S5');
    INSERT INTO Author_By_Source VALUES(1,1),(2,2),(3,3),(4,4),(5,5),(1,4),(3,4),(5,2);
    
    SELECT AuthorName, SourceName 
        FROM Author_By_Source 
            JOIN Authors ON Author_By_Source.AuthorID = Authors.AuthorID
            JOIN Sources ON Author_By_Source.SourceID = Sources.SourceID
    ;   
    
    /* Just once */
    PRAGMA foreign_keys = 0;
    
    /* Per table */
    DROP TABLE IF EXISTS Author_By_Source_new;
    DROP TABLE IF EXISTS Author_By_Source_original;
    CREATE TABLE IF NOT EXISTS `Author_By_Source_new` 
        (
            `AuthorID` INTEGER NOT NULL, 
            `SourceID` INTEGER NOT NULL, 
            PRIMARY KEY(`AuthorID`, `SourceID`), 
            FOREIGN KEY(`AuthorID`) REFERENCES `Authors`(`AuthorID`) ON UPDATE NO ACTION ON DELETE NO ACTION , 
            FOREIGN KEY(`SourceID`) REFERENCES `Sources`(`SourceID`) ON UPDATE NO ACTION ON DELETE NO ACTION 
        );
    DELETE FROM Author_By_Source_new;
    INSERT INTO Author_By_Source_new SELECT * FROM Author_By_Source;
    ALTER TABLE Author_By_Source RENAME TO Author_By_Source_original;
    ALTER TABLE Author_By_Source_new RENAME TO Author_By_Source;
    /* IF HAPPY THEN DO */
    DROP TABLE IF EXISTS Author_By_Source_original;
    
    -- ...... do the equivalent for all tables
    
    CREATE INDEX IF NOT EXISTS `index_Author_By_Source_SourceID` ON `Author_By_Source` (`SourceID`);
    CREATE INDEX IF NOT EXISTS `index_Author_By_Source_AuthorID` ON `Author_By_Source` (`AuthorID`);
    
    COMMIT;
    
    /* Just once */
    PRAGMA foreign_keys = 1;
    PRAGMA foreign_key_check;
    PRAGMA integrity_check;
    SELECT sql FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite%';
    
    SELECT AuthorName, SourceName 
        FROM Author_By_Source 
            JOIN Authors ON Author_By_Source.AuthorID = Authors.AuthorID
            JOIN Sources ON Author_By_Source.SourceID = Sources.SourceID
    ;
    

    数据库已关闭,时间戳和大小检查,然后数据库文件替换了资产文件夹中的现有文件。该应用程序已卸载,然后在 MainActivity 中使用以下内容重新运行:-

        myDB = Room.databaseBuilder(this,MyAppDatabase.class,"mydb")
                .createFromAsset("mydb")
                .allowMainThreadQueries()
                .build();
        myDB.getOpenHelper().getWritableDatabase();
    
        List<AuthorWithSources> authorWithSourcesList = myDB.allDao().getAllAuthorsWithSources();
        for (AuthorWithSources aws: authorWithSourcesList) {
            Log.d("AUTHORSOURCEINFO","Author = " + aws.authors.getAuthorName() + "Source = " + aws.sources.getSourceName());
        }
    }
    
    • MyAppDatabase 是 @Database 类。
    • allowMainThreadQueries 用于方便。
    • myDB.getOpenHelper().getWritableDatabase(); 用于强制立即打开并因此复制数据库。

    • getAllAuthorsWithSources() 按照它所说的使用 @Query("SELECT * FROM Author_By_Source") List<AuthorWithSources> getAllAuthorsWithSources();

    AuthorWithSources 是:-

    public class AuthorWithSources {
    
        @Embedded
        AuthorBySource authorBySource;
        @Relation(entity = Authors.class,parentColumn = "AuthorID",entityColumn = "AuthorID")
        Authors authors;
        @Relation(entity = Sources.class,parentColumn = "SourceID",entityColumn = "SourceID")
        Sources sources;
    }
    

    结果

    没有错误并且在日志中:-

    2020-01-16 10:09:23.623 D/AUTHORSOURCEINFO: Author = FredSource = S1
    2020-01-16 10:09:23.623 D/AUTHORSOURCEINFO: Author = MarySource = S2
    2020-01-16 10:09:23.623 D/AUTHORSOURCEINFO: Author = JoanSource = S3
    2020-01-16 10:09:23.623 D/AUTHORSOURCEINFO: Author = BertSource = S4
    2020-01-16 10:09:23.623 D/AUTHORSOURCEINFO: Author = AlanSource = S5
    2020-01-16 10:09:23.623 D/AUTHORSOURCEINFO: Author = FredSource = S4
    2020-01-16 10:09:23.623 D/AUTHORSOURCEINFO: Author = JoanSource = S4
    2020-01-16 10:09:23.623 D/AUTHORSOURCEINFO: Author = AlanSource = S2
    

    替代方法

    存在一个工具,作为一个应用程序,可以转换数据库以供 Room 使用,并为实体/道生成 java 代码。

    这里有详细的工具RoomExistingSQLiteDBConverter

    使用工具和

    • 将生成的java源代码复制到项目中,然后
    • 访问每个文件并解析导入(假设输入了正确的包名称,否则还必须调整或添加包)然后
    • 在项目中创建assets文件夹,然后
    • 复制生成的数据库
    • 最后创建代码以使用数据库,例如

      soanswersDatabase = Room.databaseBuilder(this,SoanswersDatabase.class,SoanswersDatabase.DBNAME)
              .allowMainThreadQueries()
              .createFromAsset(SoanswersDatabase.DBNAME)
              .build();
      List<Author_By_Source> authorBySourceList = soanswersDatabase.getAuthor_By_SourceDao().getEveryAuthor_By_Source();
      for (Author_By_Source abs: authorBySourceList) {
          Log.d("AUTHORSOURCEINFO",
                  "AuthorReference = " + String.valueOf(abs.getAuthorId())
                  + " Sourcereference = " + String.valueOf(abs.getSourceId())
          );
      }
      
    • 即上面是唯一输入的代码。 结果:-
    2020-01-16 10:46:27.907 D/AUTHORSOURCEINFO: AuthorReference = 1 Sourcereference = 1
    2020-01-16 10:46:27.907 D/AUTHORSOURCEINFO: AuthorReference = 2 Sourcereference = 2
    2020-01-16 10:46:27.907 D/AUTHORSOURCEINFO: AuthorReference = 3 Sourcereference = 3
    2020-01-16 10:46:27.907 D/AUTHORSOURCEINFO: AuthorReference = 4 Sourcereference = 4
    2020-01-16 10:46:27.907 D/AUTHORSOURCEINFO: AuthorReference = 5 Sourcereference = 5
    2020-01-16 10:46:27.907 D/AUTHORSOURCEINFO: AuthorReference = 1 Sourcereference = 4
    2020-01-16 10:46:27.907 D/AUTHORSOURCEINFO: AuthorReference = 3 Sourcereference = 4
    2020-01-16 10:46:27.907 D/AUTHORSOURCEINFO: AuthorReference = 5 Sourcereference = 2
    

    【讨论】:

    • 这看起来很有用,但我需要时间来吸收和理解它。在这一点上,我所做的只是创建所有必要的带注释的类(例如实体、DAO、数据库)和几个带有嵌入式、关系和连接的类用于查询。我还没有完全了解预填充数据库的迁移和转换完全没有。
    【解决方案2】:

    我能够解决这个问题,但它相当于将外部数据库字段约束与 Android 房间实体匹配,并了解更多关于房间注释的信息。问题基本上是该字段是否为空。出于某种原因,这对我来说可能是一个天真的误解,当使用 Android Room 创建实体时,字段似乎会自动默认为“notNull”,除非我指定,否则我个人不会想到这一点。但是,根据经验,我认为我拥有一个比我应该拥有的更稳定的数据库。

    【讨论】:

    • 那么你做了什么来处理这个问题?因为现在我的表中的随机字段被标记为“预期的 notNull”,即使它们以前从未被标记为 notNull。有什么方法可以告诉 Room 期望这些特定字段为 notNull?
    • 请记住,我使用的是预先填充的数据。我花了很多时间比较 EXPECTED 模式结果和 FOUND 模式结果,或者操作特定房间实体上的约束(即注释)或编辑预填充数据库的表字段上的约束我正在使用。大多数时候我编辑预先填充的表格。你可能需要做同样的事情,但这就是我为每个表/实体所做的。
    猜你喜欢
    • 2021-03-02
    • 1970-01-01
    • 2020-03-05
    • 1970-01-01
    • 1970-01-01
    • 2020-06-03
    • 2018-06-27
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多