使用 2.3.0-alpha03 版本的房间有一个prepackagedDatabaseCallback
它说:-
-
此回调将在复制预包 DB 之后但在 Room 有机会打开它之前调用,因此在调用 RoomDatabase.Callback 方法之前。此回调可用于更新预打包 DB 模式以满足 Room 的模式验证。
那么我怎么能用它来规避预期的无效架构......找到......?
我可以用它来介绍触发器,因为房间没有用于创建触发器的注释吗?
请注意,这旨在作为询问和回答您自己的问题
使用 2.3.0-alpha03 版本的房间有一个prepackagedDatabaseCallback
它说:-
此回调将在复制预包 DB 之后但在 Room 有机会打开它之前调用,因此在调用 RoomDatabase.Callback 方法之前。此回调可用于更新预打包 DB 模式以满足 Room 的模式验证。
那么我怎么能用它来规避预期的无效架构......找到......?
我可以用它来介绍触发器,因为房间没有用于创建触发器的注释吗?
请注意,这旨在作为询问和回答您自己的问题
那么我怎么能用它来规避预期的无效架构......找到......?
下面虽然冗长,但显示了一个相应地更正模式的示例。
我可以用它来介绍触发器,因为房间没有用于创建触发器的注释吗?
是的,虽然这实际上没有经过测试,但该示例适合创建触发器。
对示例进行重新评分的说明 首先,应该注意的是,最初查看回调时存在问题。现在已经解决了这些问题,但需要2.4.0-beta02或更高版本。问题是: -
应用的修复是
在 PrepackagedDatabaseCallback 调用期间正确打开预打包数据库。
实际的预包数据库没有被打开,因为在创建临时打开帮助程序时只使用了文件名而不是完整路径。如果数据库的名称是路径(以“/”开头),框架打开帮助程序只会打开现有文件,否则它会创建一个新文件。此更改通过使用绝对文件路径作为名称来修复它。
流动
当控制权传递给回调时,资产数据库已被复制。因此,理论上,这只是改变 TABLES 和 VIEWS(如果有的话)以匹配房间期望的模式的问题。
通常试图将房间的期望与房间的发现相匹配会让一些人感到沮丧。下面的示例将克服次要/小/经常遗漏的问题。它的工作原理是:-
INSERT OR IGNORE
just INSERT
to fail)它确实希望源(资产)具有正确顺序的列,空值不包含在房间具有 NOT NULL 约束的列中。但是,由于使用了 INSERT OR IGNORE,因此不会插入此类行,而不是导致异常。
除了从生成的 java 复制代码之外,该过程是自动化的,并且应该无需修改即可处理大多数/许多资产。
编码
绝大多数代码都在 @Database 类OtherDatabase中。请注意,这应该可以通过一些调整来应对许多数据库(请参阅评论以进行更改):-
@SuppressLint({"Range"}) /*<<<<< used due to bug/issue with getColumnIndex introduced with SDK 31 */
@Database(
entities = {Person.class, Company.class, CompanyPersonMap.class}, //<<<<< CHANGED ACCORDINGLY
version = OtherDatabase.DATABASE_VERSION, /* note due to shared usage of DBVERSION OtherDatabase used <<<<< CHANGE ACCORDINGLY */
exportSchema = false /* change to suit */
)
@TypeConverters({AllTypeConverters.class})
abstract class OtherDatabase extends RoomDatabase {
public static final String DATABASE_NAME = "otherprepackageddatabasecallbacktest.db"; //<<<<< CHANGE AS REQUIRED
public static final int DATABASE_VERSION = 1; //<<<<< CHANGE AS REQUIRED
public static final String ASSET_FILE_NAME = "prepackageddatabasecallbacktest.db"; //<<<<< CHANGED AS REQUIRED
/**
* sqlite_master table and column names !!!!!DO NOT CHANGE!!!!!
*/
private static final String SQLITE_MASTER_TABLE_NAME = "sqlite_master";
private static final String SQLITE_MASTER_COL_NAME = "name";
private static final String SQLITE_MASTER_COL_TYPE = "type";
private static final String SQLITE_MASTER_COL_TABLE_NAME = "tbl_name";
private static final String SQLITE_MASTER_COL_SQL = "sql";
abstract AllDao getAllDao(); //<<<<< CHANGE ACCORDINGLY
private static volatile OtherDatabase instance = null; //<<<<< CHANGE ACCORDINGLY
public static OtherDatabase /*<<<< CHANGE ACCORDINGLY */ getInstance(Context context) {
if (instance == null) {
instance = Room.databaseBuilder(context, OtherDatabase.class, OtherDatabase.DATABASE_NAME)
.allowMainThreadQueries() /*<<<<< USED FOR BREVITY CONVENIENCE */
.createFromAsset(ASSET_FILE_NAME, prePkgDbCallback) /* 2nd parameter is the THE CALL BACK to invoke */
.build();
}
return instance;
}
/* THE CALLBACK */
static final PrepackagedDatabaseCallback prePkgDbCallback = new PrepackagedDatabaseCallback() {
final static String assetTablePrefix = "asset_"; /* prefix used for renamed tables - should not need to be changed */
private List<SQLiteMasterComponent> sqLiteMasterComponentArray; /* store for sqlite_master extract */
@Override
public void onOpenPrepackagedDatabase(@NonNull SupportSQLiteDatabase db) {
super.onOpenPrepackagedDatabase(db);
sqLiteMasterComponentArray = buildComponentsList(db); /* gets relevant rows from sqlite_master */
dropNonTableComponents(sqLiteMasterComponentArray,db); /* everything except the tables */
renameTableComponents(sqLiteMasterComponentArray, assetTablePrefix,db); /* rename the tables using prefix */
/*<<<<< TAILOR (the db.execSQL's below) AS APPROPRIATE - SEE COMMENTS THAT FOLLOW >>>>>*/
/* copied from the @Database classes generated java e.g. leave indexes till later
_db.execSQL("CREATE TABLE IF NOT EXISTS `Person` (`personid` INTEGER, `firstName` TEXT, `lastName` TEXT, `middleNames` TEXT, `dateOfBirth` INTEGER, PRIMARY KEY(`personid`))");
_db.execSQL("CREATE INDEX IF NOT EXISTS `index_Person_firstName` ON `Person` (`firstName`)");
_db.execSQL("CREATE INDEX IF NOT EXISTS `index_Person_lastName` ON `Person` (`lastName`)");
_db.execSQL("CREATE TABLE IF NOT EXISTS `company` (`companyid` INTEGER, `companyName` TEXT, `city` TEXT, `state` TEXT, `country` TEXT, `notes` TEXT, PRIMARY KEY(`companyid`))");
_db.execSQL("CREATE TABLE IF NOT EXISTS `company_person_map` (`companyid_map` INTEGER NOT NULL, `personid_map` INTEGER NOT NULL, PRIMARY KEY(`companyid_map`, `personid_map`), FOREIGN KEY(`companyid_map`) REFERENCES `company`(`companyid`) ON UPDATE CASCADE ON DELETE CASCADE , FOREIGN KEY(`personid_map`) REFERENCES `Person`(`personid`) ON UPDATE CASCADE ON DELETE CASCADE )");
_db.execSQL("CREATE INDEX IF NOT EXISTS `index_company_person_map_personid_map` ON `company_person_map` (`personid_map`)");
*/
/* Create the tables as per Room definitions - ***** CREATE TABLES COPIED FROM GENERATED JAVA *****
only indexes, views, triggers (for FTS) should be done after the data has been copied so :-
data is loaded faster as no index updates are required.
triggers don't get triggered when loading the data which could result in unexpected results
*/
db.execSQL("CREATE TABLE IF NOT EXISTS `Person` (`personid` INTEGER, `firstName` TEXT, `lastName` TEXT, `middleNames` TEXT, `dateOfBirth` INTEGER, PRIMARY KEY(`personid`));");
db.execSQL("CREATE TABLE IF NOT EXISTS `company` (`companyid` INTEGER, `companyName` TEXT, `city` TEXT, `state` TEXT, `country` TEXT, `notes` TEXT, PRIMARY KEY(`companyid`))");
db.execSQL("CREATE TABLE IF NOT EXISTS `company_person_map` (`companyid_map` INTEGER NOT NULL, `personid_map` INTEGER NOT NULL, PRIMARY KEY(`companyid_map`, `personid_map`), FOREIGN KEY(`companyid_map`) REFERENCES `company`(`companyid`) ON UPDATE CASCADE ON DELETE CASCADE , FOREIGN KEY(`personid_map`) REFERENCES `Person`(`personid`) ON UPDATE CASCADE ON DELETE CASCADE )");
copyData(sqLiteMasterComponentArray, assetTablePrefix,db); /* copy the data from the renamed asset tables to the newly created Room tables */
/* Create the other Room components - ***** CREATE ? COPIED FROM GENERATED JAVA *****
Now that data has been copied create other Room components indexes, views and triggers
(triggers would only be for FTS (full text search))
again sourced from generated Java
*/
db.execSQL("CREATE INDEX IF NOT EXISTS `index_Person_firstName` ON `Person` (`firstName`)");
db.execSQL("CREATE INDEX IF NOT EXISTS `index_Person_lastName` ON `Person` (`lastName`)");
db.execSQL("CREATE INDEX IF NOT EXISTS `index_company_person_map_personid_map` ON `company_person_map` (`personid_map`)");
dropRenamedTableComponents(sqLiteMasterComponentArray, assetTablePrefix,db); /* done with the renamed tables so drop them */
db.execSQL("VACUUM"); /* cleanup the database */
createTriggers(sqLiteMasterComponentArray,db); /* create any triggers */
}
};
static int dropNonTableComponents(List<SQLiteMasterComponent> components, SupportSQLiteDatabase db) {
int rv = 0;
for(SQLiteMasterComponent c: components) {
if (!c.type.equals("table") ) {
db.execSQL("DROP " + c.type + " IF EXISTS " + c.name);
rv++;
}
}
return rv;
}
static int dropRenamedTableComponents(List<SQLiteMasterComponent> components, String prefix, SupportSQLiteDatabase db) {
int rv = 0;
int maxForeignKeyCount = 0;
for (SQLiteMasterComponent c: components) {
if (c.type.equals("table") && c.foreignKeyCount > maxForeignKeyCount) {
maxForeignKeyCount = c.foreignKeyCount;
}
}
for (int i= maxForeignKeyCount; i >= 0; i--) {
for (SQLiteMasterComponent c: components) {
if (c.type.equals("table") && c.foreignKeyCount == i) {
db.execSQL("DROP " + c.type + " IF EXISTS " + prefix + c.name);
rv++;
}
}
}
return rv;
}
static int renameTableComponents(List<SQLiteMasterComponent> components, String prefix, SupportSQLiteDatabase db) {
int rv = 0;
db.execSQL("PRAGMA foreign_keys = ON"); // Just in case turn foreign keys on
for(SQLiteMasterComponent c: components) {
if (c.type.equals("table")) {
db.execSQL("ALTER TABLE " + c.name + " RENAME TO " + prefix + c.name);
rv++;
}
}
return rv;
}
/*
NOTE tables with fewest Foreign Key definitions done first
NOTE makes an assumption that this will not result in FK conflicts
TODO should really be amended to ensure that a table with FK's is only attempted when all of it's parent tables have been loaded
*/
static int copyData(List<SQLiteMasterComponent> components, String prefix, SupportSQLiteDatabase db) {
int rv = 0;
int maxForeignKeyCount = 0;
for (SQLiteMasterComponent c: components) {
if (c.type.equals("table") && c.foreignKeyCount > 0) {
maxForeignKeyCount = c.foreignKeyCount;
}
}
for (int i=0; i <= maxForeignKeyCount; i++) {
for (SQLiteMasterComponent c: components) {
if (c.type.equals("table") && c.foreignKeyCount == i) {
db.execSQL("INSERT OR IGNORE INTO " + c.name + " SELECT * FROM " + prefix + c.name + ";");
rv++;
}
}
}
return rv;
}
static int createTriggers(List<SQLiteMasterComponent> components, SupportSQLiteDatabase db) {
int rv = 0;
for (SQLiteMasterComponent c: components) {
if (c.type.equals("trigger")) {
//TODO should really check if the sql includes IF NOT EXISTSand if not add IF NOT EXISTS
db.execSQL(c.sql);
rv++;
}
}
return rv;
}
/**
* Build the list of required SQLiteMasterComponents to save having to access
* sqlite_master many times.
* @param db the SupportSQliteDatabase to access
* @return the list of SQliteMasterComponents extracted
*/
@SuppressLint("Range")
static List<SQLiteMasterComponent> buildComponentsList(SupportSQLiteDatabase db) {
final String FOREIGN_KEY_FLAG_COLUMN = "foreign_key_flag";
ArrayList<SQLiteMasterComponent> rv = new ArrayList<>();
Cursor csr = db.query("SELECT *," +
/* Column to indicate wherther or not FK constraints appear to have been defined
* NOTE!! can be fooled
* e.g. if a column is defined as `my badly named FOREIGN KEY column ` ....
* */
"(" +
"instr(" + SQLITE_MASTER_COL_SQL + ",'FOREIGN KEY') > 0) + " +
"(instr(" + SQLITE_MASTER_COL_SQL + ",' REFERENCES ')> 0) " +
"AS " + FOREIGN_KEY_FLAG_COLUMN + " " +
"FROM " + SQLITE_MASTER_TABLE_NAME + " " +
/* do not want any sqlite tables or android tables included */
"WHERE lower(" + SQLITE_MASTER_COL_NAME + ") NOT LIKE 'sqlite_%' AND lower(" + SQLITE_MASTER_COL_NAME + ") NOT LIKE 'android_%'");
while (csr.moveToNext()) {
SQLiteMasterComponent component = new SQLiteMasterComponent(
csr.getString(csr.getColumnIndex(SQLITE_MASTER_COL_NAME)),
csr.getString(csr.getColumnIndex(SQLITE_MASTER_COL_TYPE)),
csr.getString(csr.getColumnIndex(SQLITE_MASTER_COL_TABLE_NAME)),
csr.getString(csr.getColumnIndex(SQLITE_MASTER_COL_SQL)),
csr.getInt(csr.getColumnIndex(FOREIGN_KEY_FLAG_COLUMN)),
0);
if (csr.getInt(csr.getColumnIndex(FOREIGN_KEY_FLAG_COLUMN)) > 0) {
component.foreignKeyCount = component.getForeignKeyCount(db);
}
rv.add(component);
}
csr.close();
return (List<SQLiteMasterComponent>) rv;
}
/**
* Class to hold a row from sqlite_master
*/
private static class SQLiteMasterComponent {
private String name;
private String type;
private String owningTable;
private String sql;
private int foreignKeyFlag = 0;
private int foreignKeyCount = 0;
SQLiteMasterComponent(String name, String type, String owningTable, String sql, int foreignKeyFlag, int foreignKeyCount) {
this.name = name;
this.type = type;
this.owningTable = owningTable;
this.sql = sql;
this.foreignKeyFlag = foreignKeyFlag;
this.foreignKeyCount = foreignKeyCount;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getOwningTable() {
return owningTable;
}
public void setOwningTable(String owningTable) {
this.owningTable = owningTable;
}
public String getSql() {
return sql;
}
public void setSql(String sql) {
this.sql = sql;
}
public int getForeignKeyFlag() {
return foreignKeyFlag;
}
public void setForeignKeyFlag(int foreignKeyFlag) {
this.foreignKeyFlag = foreignKeyFlag;
}
public boolean isForeignKey() {
return this.foreignKeyFlag > 0;
}
public int getForeignKeyCount() {
return foreignKeyCount;
}
public void setForeignKeyCount(int foreignKeyCount) {
this.foreignKeyCount = foreignKeyCount;
}
/**
* Retrieve the number of rows returned by PRAGMA foreign_key_list
* @param db The SupportSQLiteDatabase to access
* @return The number of rows i.e. number of Foreign Key constraints
*/
private int getForeignKeyCount(SupportSQLiteDatabase db) {
int rv =0;
Cursor csr = db.query("SELECT count(*) FROM pragma_foreign_key_list('" + this.name + "');");
if (csr.moveToFirst()) {
rv = csr.getInt(0);
}
csr.close();
return rv;
}
}
}
工作示例
资产数据库
资产数据库包含 3 个表,person,company和company_person_map
人DDL 是
CREATE TABLE person (personid INTEGER PRIMARY KEY, firstName TEXT, lastName TEXT, middleNames TEXT, dateOfBirth DATE);
房间期望:-
CREATE TABLE IF NOT EXISTS `Person` (`personid` INTEGER, `firstName` TEXT, `lastName` TEXT, `middleNames` TEXT, `dateOfBirth` INTEGER, PRIMARY KEY(`personid`))
公司DDL 是
CREATE TABLE company (companyid INTEGER PRIMARY KEY, companyName TEXT, city TEXT, state TEXT, country TEXT, notes TEXT);
房间期望:-
CREATE TABLE IF NOT EXISTS `company` (`companyid` INTEGER, `companyName` TEXT, `city` TEXT, `state` TEXT, `country` TEXT, `notes` TEXT, PRIMARY KEY(`companyid`))
company_person_map DDL 是
CREATE TABLE company_person_map (
companyid_map INTEGER NOT NULL REFERENCES company(companyid) ON DELETE CASCADE ON UPDATE CASCADE,
personid_map INTEGER NOT NULL REFERENCES person(personid) ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY (companyid_map, personid_map));
房间期望:-
CREATE TABLE IF NOT EXISTS `company_person_map` (
`companyid_map` INTEGER NOT NULL,`personid_map` INTEGER NOT NULL,
PRIMARY KEY(`companyid_map`, `personid_map`),
FOREIGN KEY(`companyid_map`) REFERENCES `company`(`companyid`) ON UPDATE CASCADE ON DELETE CASCADE ,
FOREIGN KEY(`personid_map`) REFERENCES `Person`(`personid`) ON UPDATE CASCADE ON DELETE CASCADE )
资产表包含以下数据:-
人
公司
company_person_map
AllDao是
调用活动是: -
public class MainActivity extends AppCompatActivity {
OtherDatabase dbOther;
AllDao daoOther;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
dbOther = OtherDatabase.getInstance(this);
daoOther = dbOther.getAllDao();
for(Person p: daoOther.getAllPeople()) {
Log.d("DBINFO",
"Person is " + p.firstName
+ " " + p.middleNames
+ " " + p.lastName
+ " Date of Birth is " + p.dateOfBirth
+ " ID is " + p.id
);
}
for(CompanyWithPeople cwp: dao.getCompanyWithPeople()) {
logCompany(cwp.company,"DBINFO","");
for(Person p: cwp.person) {
logPerson(p,"DBINFO","\n\t");
}
}
}
void logCompany(Company c, String tag, String lineFeed) {
Log.d(tag,lineFeed + "Company is " + c.companyName
+ " Location is " + c.city + ", " + c.state + ", " + c.country
+ " ID is " + c.companyId
+ "\n\t Notes: " + c.notes
);
}
void logPerson(Person p, String tag, String lineFeed) {
Log.d(tag, lineFeed + p.firstName + " " + p.middleNames
+ " " + p.lastName + " Date of Birth is " + p.dateOfBirth
+ " ID is " + p.id
);
}
}
结果
2021-11-28 19:56:01.928 D/DBINFO: Person is Robert John Smith Date of Birth is Sat Dec 27 17:46:33 GMT+10:00 1969 ID is 1
2021-11-28 19:56:01.929 D/DBINFO: Person is Julie Mary Armstrong Date of Birth is Mon Jan 12 23:22:04 GMT+10:00 1970 ID is 2
2021-11-28 19:56:01.929 D/DBINFO: Person is Andrea Susan Stewart Date of Birth is Mon Jan 05 04:56:09 GMT+10:00 1970 ID is 3
2021-11-28 19:56:01.929 D/DBINFO: Person is Mary Belinda Allway Date of Birth is Mon Jan 12 00:15:21 GMT+10:00 1970 ID is 4
2021-11-28 19:56:01.929 D/DBINFO: Person is Lisa Elizabeth Brooks Date of Birth is Sat Jan 03 03:51:21 GMT+10:00 1970 ID is 5
2021-11-28 19:56:01.930 D/DBINFO: Person is Stephen Colin Cobbs Date of Birth is Tue Jan 06 14:01:55 GMT+10:00 1970 ID is 6
2021-11-28 19:56:01.930 D/DBINFO: Person is Breane Cath Davidson Date of Birth is Thu Jan 01 22:30:14 GMT+10:00 1970 ID is 7
2021-11-28 19:56:01.930 D/DBINFO: Person is Trevor Arthur Frankston Date of Birth is Sat Jan 10 03:47:02 GMT+10:00 1970 ID is 8
2021-11-28 19:56:01.930 D/DBINFO: Person is George Howard Erksine Date of Birth is Sun Jan 11 00:47:02 GMT+10:00 1970 ID is 9
2021-11-28 19:56:01.930 D/DBINFO: Person is Uriah Stanley Jefferson Date of Birth is Mon Dec 29 19:11:31 GMT+10:00 1969 ID is 10
2021-11-28 19:56:01.931 D/DBINFO: Person is Valerie Alana Singleton Date of Birth is Thu Jan 01 11:45:07 GMT+10:00 1970 ID is 11
2021-11-28 19:56:01.931 D/DBINFO: Person is Vladimir Oscar Whitworth Date of Birth is Sat Jan 10 00:29:45 GMT+10:00 1970 ID is 12
2021-11-28 19:56:01.936 D/DBINFO: Company is Allbright Construction Location is Sydney, NSW, Australia ID is 1
Notes:
2021-11-28 19:56:01.936 D/DBINFO: Julie Mary Armstrong Date of Birth is Mon Jan 12 23:22:04 GMT+10:00 1970 ID is 2
2021-11-28 19:56:01.936 D/DBINFO: Mary Belinda Allway Date of Birth is Mon Jan 12 00:15:21 GMT+10:00 1970 ID is 4
2021-11-28 19:56:01.937 D/DBINFO: Stephen Colin Cobbs Date of Birth is Tue Jan 06 14:01:55 GMT+10:00 1970 ID is 6
2021-11-28 19:56:01.937 D/DBINFO: Trevor Arthur Frankston Date of Birth is Sat Jan 10 03:47:02 GMT+10:00 1970 ID is 8
2021-11-28 19:56:01.937 D/DBINFO: Uriah Stanley Jefferson Date of Birth is Mon Dec 29 19:11:31 GMT+10:00 1969 ID is 10
2021-11-28 19:56:01.937 D/DBINFO: Vladimir Oscar Whitworth Date of Birth is Sat Jan 10 00:29:45 GMT+10:00 1970 ID is 12
2021-11-28 19:56:01.937 D/DBINFO: Company is Dextronics Location is Slough, Berkshire, England ID is 2
Notes:
2021-11-28 19:56:01.937 D/DBINFO: Robert John Smith Date of Birth is Sat Dec 27 17:46:33 GMT+10:00 1969 ID is 1
2021-11-28 19:56:01.938 D/DBINFO: Andrea Susan Stewart Date of Birth is Mon Jan 05 04:56:09 GMT+10:00 1970 ID is 3
2021-11-28 19:56:01.938 D/DBINFO: Lisa Elizabeth Brooks Date of Birth is Sat Jan 03 03:51:21 GMT+10:00 1970 ID is 5
2021-11-28 19:56:01.938 D/DBINFO: Breane Cath Davidson Date of Birth is Thu Jan 01 22:30:14 GMT+10:00 1970 ID is 7
2021-11-28 19:56:01.938 D/DBINFO: George Howard Erksine Date of Birth is Sun Jan 11 00:47:02 GMT+10:00 1970 ID is 9