尽管未使用 createFromFile/Asset,但预打包数据库的架构无效

Pre-packaged database has an invalid schema DESPITE not using createFromFile/Asset

我一直在到处寻找这个错误的原因:

TableInfo{name='recipes', columns={cook_time=Column{name='cook_time', type='INTEGER', affinity='3', notNull=true, primaryKeyPosition=0, defaultValue='null'}, tier_rating=Column{name='tier_rating', type='INTEGER', affinity='3', notNull=true, primaryKeyPosition=0, defaultValue='null'}, notes=Column{name='notes', type='TEXT', affinity='2', notNull=false, primaryKeyPosition=0, defaultValue='null'}, tom_rating=Column{name='tom_rating', type='INTEGER', affinity='3', notNull=true, primaryKeyPosition=0, defaultValue='null'}, prep_time=Column{name='prep_time', type='INTEGER', affinity='3', notNull=true, primaryKeyPosition=0, defaultValue='null'}, serves=Column{name='serves', type='INTEGER', affinity='3', notNull=true, primaryKeyPosition=0, defaultValue='1'}, name=Column{name='name', type='TEXT', affinity='2', notNull=true, primaryKeyPosition=0, defaultValue='null'}, id=Column{name='id', type='INTEGER', affinity='3', notNull=true, primaryKeyPosition=1, defaultValue='null'}, url=Column{name='url', type='TEXT', affinity='2', notNull=false, primaryKeyPosition=0, defaultValue='null'}}, foreignKeys=[], indices=[Index{name='index_recipes_name', unique=true, columns=[name]}]}
     Found:
TableInfo{name='recipes', columns={}, foreignKeys=[], indices=[]}
at androidx.room.RoomOpenHelper.checkIdentity(RoomOpenHelper.java:163)
    at androidx.room.RoomOpenHelper.onOpen(RoomOpenHelper.java:135)
    at androidx.sqlite.db.framework.FrameworkSQLiteOpenHelper$OpenHelper.onOpen(FrameworkSQLiteOpenHelper.java:195)
    at android.database.sqlite.SQLiteOpenHelper.getDatabaseLocked(SQLiteOpenHelper.java:428)
    at android.database.sqlite.SQLiteOpenHelper.getWritableDatabase(SQLiteOpenHelper.java:317)
    at androidx.sqlite.db.framework.FrameworkSQLiteOpenHelper$OpenHelper.getWritableSupportDatabase(FrameworkSQLiteOpenHelper.java:145)
    at androidx.sqlite.db.framework.FrameworkSQLiteOpenHelper.getWritableDatabase(FrameworkSQLiteOpenHelper.java:106)
    at androidx.room.RoomDatabase.inTransaction(RoomDatabase.java:622)
    at androidx.room.RoomDatabase.assertNotSuspendingTransaction(RoomDatabase.java:399)
    at androidx.room.RoomDatabase.query(RoomDatabase.java:442)
    at androidx.room.util.DBUtil.query(DBUtil.java:83)
    at com.example.shoppinglistapp2.db.dao.IngListItemDao_Impl.call(IngListItemDao_Impl.java:1036)
    at com.example.shoppinglistapp2.db.dao.IngListItemDao_Impl.call(IngListItemDao_Impl.java:1033)
    at androidx.room.RoomTrackingLiveData.run(RoomTrackingLiveData.java:90)

每个其他类似问题似乎至少有一些 table 元素,或者正在使用 databaseBuilder.createFromAsset 功能。然而我不是,我的数据库代码粘贴在下面。我试过删除所有迁移,然后退回到破坏性迁移。我查看了导出的架构文件,这些文件清楚地正确显示了所有属性。在我的应用程序中一切正常,直到我尝试将它安装在没有现有数据库实例的新设备上。我可以 运行 在具有来自应用程序之前 builds/use 的现有数据库数据的设备上正常运行,但是全新安装在启动时立即崩溃,并且出现上述错误。我之前没有尝试过这个,所以我不知道这个问题可能是在什么时候引入的。

我使用的是 Room 版本“2.4.0-alpha01”,但我也试过回到 2.3.0,没有任何区别。 是否有一些外部库试图预填充我不知道的内容?我不确定它甚至在哪里找到空 tables 出现在“已找到”部分中的架构 - 我什至没有它可能试图获取的 /assets 文件夹 createFromAsset资源来自即使我正在调用它。

SlaDatabase.java

@Database(entities = {Recipe.class, IngList.class, IngListItem.class, Tag.class, MealPlan.class, Meal.class},
        version = 17,
        exportSchema = true
//        autoMigrations = {
//                @AutoMigration(from = 15,to = 16),
//                @AutoMigration(from = 16, to = 17)
//        }
)
public abstract class SlaDatabase extends RoomDatabase {
    public abstract RecipeDao recipeDao();
    public abstract IngListItemDao ingListItemDao();
    public abstract MealDao mealDao();
    public abstract TagDao tagDao();
    public abstract MealPlanDao mealPlanDao();
    public abstract IngListDao ingListDao();

    private static volatile SlaDatabase INSTANCE;
    private static final int NUMBER_OF_THREADS = 4;
    static final ListeningExecutorService databaseWriteExecutor =
            MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(NUMBER_OF_THREADS));

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

    //ensure the shopping list IngList (id = 0) exists
    private static RoomDatabase.Callback sRoomDatabaseCallback = new RoomDatabase.Callback(){
        @Override
        public void onCreate(@NonNull SupportSQLiteDatabase db) {
            super.onCreate(db);
            db.beginTransaction();
            try {
                db.execSQL("INSERT OR IGNORE INTO ing_lists DEFAULT VALUES");
            } catch(Exception e){
                Log.d("TOM_TEST", e.toString());;
            }
            finally {
                db.endTransaction();
            }
        }
    };

Recipe.java

@Entity(
        tableName = "recipes",
        indices = {@Index(value = {"name"}, unique = true)}//the recipe name must be unique
)
public class Recipe {

    @PrimaryKey(autoGenerate = true)
    private int id;

    @NonNull
    private String name;

    @ColumnInfo(name="prep_time")
    private int prepTime;

    @ColumnInfo(name = "cook_time")
    private int cookTime;

    @ColumnInfo(defaultValue = "1")
    private int serves = 1;

    private String url;
    private String notes;

    private int tom_rating;
    private int tier_rating;
    //getters and setters...

SlaDatabase 版本 17 的导出模式

{
  "formatVersion": 1,
  "database": {
    "version": 17,
    "identityHash": "fe979e62da4f94c5bb89144e955361e0",
    "entities": [
      {
        "tableName": "recipes",
        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `prep_time` INTEGER NOT NULL, `cook_time` INTEGER NOT NULL, `serves` INTEGER NOT NULL DEFAULT 1, `url` TEXT, `notes` TEXT, `tom_rating` INTEGER NOT NULL, `tier_rating` INTEGER NOT NULL)",
        "fields": [
          {
            "fieldPath": "id",
            "columnName": "id",
            "affinity": "INTEGER",
            "notNull": true
          },
          {
            "fieldPath": "name",
            "columnName": "name",
            "affinity": "TEXT",
            "notNull": true
          },
          {
            "fieldPath": "prepTime",
            "columnName": "prep_time",
            "affinity": "INTEGER",
            "notNull": true
          },
          {
            "fieldPath": "cookTime",
            "columnName": "cook_time",
            "affinity": "INTEGER",
            "notNull": true
          },
          {
            "fieldPath": "serves",
            "columnName": "serves",
            "affinity": "INTEGER",
            "notNull": true,
            "defaultValue": "1"
          },
          {
            "fieldPath": "url",
            "columnName": "url",
            "affinity": "TEXT",
            "notNull": false
          },
          {
            "fieldPath": "notes",
            "columnName": "notes",
            "affinity": "TEXT",
            "notNull": false
          },
          {
            "fieldPath": "tom_rating",
            "columnName": "tom_rating",
            "affinity": "INTEGER",
            "notNull": true
          },
          {
            "fieldPath": "tier_rating",
            "columnName": "tier_rating",
            "affinity": "INTEGER",
            "notNull": true
          }
        ],
        "primaryKey": {
          "columnNames": [
            "id"
          ],
          "autoGenerate": true
        },
        "indices": [
          {
            "name": "index_recipes_name",
            "unique": true,
            "columnNames": [
              "name"
            ],
            "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_recipes_name` ON `${TABLE_NAME}` (`name`)"
          }
        ],
        "foreignKeys": []
      },
      {
        "tableName": "ing_lists",
        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `recipe_id` INTEGER, `meal_plan_id` INTEGER, FOREIGN KEY(`recipe_id`) REFERENCES `recipes`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`meal_plan_id`) REFERENCES `meal_plans`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
        "fields": [
          {
            "fieldPath": "id",
            "columnName": "id",
            "affinity": "INTEGER",
            "notNull": true
          },
          {
            "fieldPath": "recipeId",
            "columnName": "recipe_id",
            "affinity": "INTEGER",
            "notNull": false
          },
          {
            "fieldPath": "mealPlanId",
            "columnName": "meal_plan_id",
            "affinity": "INTEGER",
            "notNull": false
          }
        ],
        "primaryKey": {
          "columnNames": [
            "id"
          ],
          "autoGenerate": true
        },
        "indices": [
          {
            "name": "index_ing_lists_recipe_id",
            "unique": false,
            "columnNames": [
              "recipe_id"
            ],
            "createSql": "CREATE INDEX IF NOT EXISTS `index_ing_lists_recipe_id` ON `${TABLE_NAME}` (`recipe_id`)"
          },
          {
            "name": "index_ing_lists_meal_plan_id",
            "unique": false,
            "columnNames": [
              "meal_plan_id"
            ],
            "createSql": "CREATE INDEX IF NOT EXISTS `index_ing_lists_meal_plan_id` ON `${TABLE_NAME}` (`meal_plan_id`)"
          }
        ],
        "foreignKeys": [
          {
            "table": "recipes",
            "onDelete": "CASCADE",
            "onUpdate": "NO ACTION",
            "columns": [
              "recipe_id"
            ],
            "referencedColumns": [
              "id"
            ]
          },
          {
            "table": "meal_plans",
            "onDelete": "CASCADE",
            "onUpdate": "NO ACTION",
            "columns": [
              "meal_plan_id"
            ],
            "referencedColumns": [
              "id"
            ]
          }
        ]
      },
      {
        "tableName": "ing_list_items",
        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `list_id` INTEGER NOT NULL, `volume_unit` TEXT, `volume_qty` REAL NOT NULL, `mass_unit` TEXT, `mass_qty` REAL NOT NULL, `whole_item_qty` REAL NOT NULL, `other_unit` TEXT, `other_qty` REAL NOT NULL, `checked` INTEGER NOT NULL DEFAULT 0, `list_order` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`list_id`) REFERENCES `ing_lists`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
        "fields": [
          {
            "fieldPath": "id",
            "columnName": "id",
            "affinity": "INTEGER",
            "notNull": true
          },
          {
            "fieldPath": "name",
            "columnName": "name",
            "affinity": "TEXT",
            "notNull": false
          },
          {
            "fieldPath": "listId",
            "columnName": "list_id",
            "affinity": "INTEGER",
            "notNull": true
          },
          {
            "fieldPath": "volumeUnit",
            "columnName": "volume_unit",
            "affinity": "TEXT",
            "notNull": false
          },
          {
            "fieldPath": "volumeQty",
            "columnName": "volume_qty",
            "affinity": "REAL",
            "notNull": true
          },
          {
            "fieldPath": "massUnit",
            "columnName": "mass_unit",
            "affinity": "TEXT",
            "notNull": false
          },
          {
            "fieldPath": "massQty",
            "columnName": "mass_qty",
            "affinity": "REAL",
            "notNull": true
          },
          {
            "fieldPath": "wholeItemQty",
            "columnName": "whole_item_qty",
            "affinity": "REAL",
            "notNull": true
          },
          {
            "fieldPath": "otherUnit",
            "columnName": "other_unit",
            "affinity": "TEXT",
            "notNull": false
          },
          {
            "fieldPath": "otherQty",
            "columnName": "other_qty",
            "affinity": "REAL",
            "notNull": true
          },
          {
            "fieldPath": "checked",
            "columnName": "checked",
            "affinity": "INTEGER",
            "notNull": true,
            "defaultValue": "0"
          },
          {
            "fieldPath": "listOrder",
            "columnName": "list_order",
            "affinity": "INTEGER",
            "notNull": true,
            "defaultValue": "0"
          }
        ],
        "primaryKey": {
          "columnNames": [
            "id"
          ],
          "autoGenerate": true
        },
        "indices": [
          {
            "name": "index_ing_list_items_name",
            "unique": false,
            "columnNames": [
              "name"
            ],
            "createSql": "CREATE INDEX IF NOT EXISTS `index_ing_list_items_name` ON `${TABLE_NAME}` (`name`)"
          },
          {
            "name": "index_ing_list_items_list_id",
            "unique": false,
            "columnNames": [
              "list_id"
            ],
            "createSql": "CREATE INDEX IF NOT EXISTS `index_ing_list_items_list_id` ON `${TABLE_NAME}` (`list_id`)"
          }
        ],
        "foreignKeys": [
          {
            "table": "ing_lists",
            "onDelete": "CASCADE",
            "onUpdate": "NO ACTION",
            "columns": [
              "list_id"
            ],
            "referencedColumns": [
              "id"
            ]
          }
        ]
      },
      {
        "tableName": "tags",
        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `recipe_id` INTEGER NOT NULL, FOREIGN KEY(`recipe_id`) REFERENCES `recipes`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
        "fields": [
          {
            "fieldPath": "id",
            "columnName": "id",
            "affinity": "INTEGER",
            "notNull": true
          },
          {
            "fieldPath": "name",
            "columnName": "name",
            "affinity": "TEXT",
            "notNull": true
          },
          {
            "fieldPath": "recipeId",
            "columnName": "recipe_id",
            "affinity": "INTEGER",
            "notNull": true
          }
        ],
        "primaryKey": {
          "columnNames": [
            "id"
          ],
          "autoGenerate": true
        },
        "indices": [
          {
            "name": "index_tags_recipe_id",
            "unique": false,
            "columnNames": [
              "recipe_id"
            ],
            "createSql": "CREATE INDEX IF NOT EXISTS `index_tags_recipe_id` ON `${TABLE_NAME}` (`recipe_id`)"
          }
        ],
        "foreignKeys": [
          {
            "table": "recipes",
            "onDelete": "CASCADE",
            "onUpdate": "NO ACTION",
            "columns": [
              "recipe_id"
            ],
            "referencedColumns": [
              "id"
            ]
          }
        ]
      },
      {
        "tableName": "meal_plans",
        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT)",
        "fields": [
          {
            "fieldPath": "id",
            "columnName": "id",
            "affinity": "INTEGER",
            "notNull": true
          },
          {
            "fieldPath": "name",
            "columnName": "name",
            "affinity": "TEXT",
            "notNull": false
          }
        ],
        "primaryKey": {
          "columnNames": [
            "id"
          ],
          "autoGenerate": true
        },
        "indices": [],
        "foreignKeys": []
      },
      {
        "tableName": "meals",
        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `plan_id` INTEGER NOT NULL, `day_id` INTEGER NOT NULL, `day_title` TEXT, `recipe_id` INTEGER, `notes` TEXT, FOREIGN KEY(`recipe_id`) REFERENCES `recipes`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`plan_id`) REFERENCES `meal_plans`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
        "fields": [
          {
            "fieldPath": "id",
            "columnName": "id",
            "affinity": "INTEGER",
            "notNull": true
          },
          {
            "fieldPath": "planId",
            "columnName": "plan_id",
            "affinity": "INTEGER",
            "notNull": true
          },
          {
            "fieldPath": "dayId",
            "columnName": "day_id",
            "affinity": "INTEGER",
            "notNull": true
          },
          {
            "fieldPath": "dayTitle",
            "columnName": "day_title",
            "affinity": "TEXT",
            "notNull": false
          },
          {
            "fieldPath": "recipeId",
            "columnName": "recipe_id",
            "affinity": "INTEGER",
            "notNull": false
          },
          {
            "fieldPath": "notes",
            "columnName": "notes",
            "affinity": "TEXT",
            "notNull": false
          }
        ],
        "primaryKey": {
          "columnNames": [
            "id"
          ],
          "autoGenerate": true
        },
        "indices": [
          {
            "name": "index_meals_plan_id",
            "unique": false,
            "columnNames": [
              "plan_id"
            ],
            "createSql": "CREATE INDEX IF NOT EXISTS `index_meals_plan_id` ON `${TABLE_NAME}` (`plan_id`)"
          },
          {
            "name": "index_meals_recipe_id",
            "unique": false,
            "columnNames": [
              "recipe_id"
            ],
            "createSql": "CREATE INDEX IF NOT EXISTS `index_meals_recipe_id` ON `${TABLE_NAME}` (`recipe_id`)"
          }
        ],
        "foreignKeys": [
          {
            "table": "recipes",
            "onDelete": "SET NULL",
            "onUpdate": "NO ACTION",
            "columns": [
              "recipe_id"
            ],
            "referencedColumns": [
              "id"
            ]
          },
          {
            "table": "meal_plans",
            "onDelete": "CASCADE",
            "onUpdate": "NO ACTION",
            "columns": [
              "plan_id"
            ],
            "referencedColumns": [
              "id"
            ]
          }
        ]
      }
    ],
    "views": [],
    "setupQueries": [
      "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
      "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'fe979e62da4f94c5bb89144e955361e0')"
    ]
  }
}

您将在未使用 setTransactionSuccessful 的情况下结束交易。如果没有,endTransaction 将回滚更改,有效地撤消更改,包括创建 Room 已创建的 tables。

因此,由于回滚,第一个 table 检查没有发现(配方)table 与预期的(配方)table 匹配,因此 找到 不包含列等

  • 它说 预打包 数据库因为 Room 似乎假设这个不一致的数据库一定来自其他地方(它不是会创建什么房间);因此它有点误导(但它应该说什么?(修辞))。

您需要 use/apply setTransactionSuccessful() 方法,例如

private static RoomDatabase.Callback sRoomDatabaseCallback = new RoomDatabase.Callback() {
    @Override
    public void onCreate(@NonNull SupportSQLiteDatabase db) {
        super.onCreate(db);
        db.beginTransaction();
        try {
            db.execSQL("INSERT OR IGNORE INTO ing_lists DEFAULT VALUES");
        } catch (Exception e) {
            Log.d("TOM_TEST", e.toString());
        } finally {
            db.setTransactionSuccessful(); //<<<<<<<<<<
            db.endTransaction();
        }
    }
};

另一种解决方案是不使用事务,例如

private static RoomDatabase.Callback sRoomDatabaseCallback = new RoomDatabase.Callback() {
    @Override
    public void onCreate(@NonNull SupportSQLiteDatabase db) {
        super.onCreate(db);
        try {
            db.execSQL("INSERT OR IGNORE INTO ing_lists DEFAULT VALUES");
        } catch (Exception e) {
            Log.d("TOM_TEST", e.toString());
        }
    }
};

没有缺点,因为执行单个 SQL 语句本身就是一个事务。