发出预填充房间数据库 - 数据库检查器为空

Issue Prepopulate Room Database - Database Inspector empty

我是 Android 开发的新手,我正在尝试开发一个非常简单的应用程序。 我想创建一个预填充的数据库,其中有一个 table 。 table 是关于用户的,它只有三列 id、name 和 profession。 该数据库的唯一用途是通过每个用户的姓名进行搜索并找到他们的职业。 所以我只需要用一些数据预先填充它。

我的问题是当我 运行 数据库时没有任何反应 没有任何反应甚至没有创建数据库。在数据库检查器中看不到任何东西

正如我从 Room 数据库的文档中读到的那样 https://developer.android.com/training/data-storage/room/prepopulate 我只需要添加以下代码

Room.databaseBuilder(appContext, AppDatabase.class, "Sample.db")
    .createFromAsset("database/myapp.db")
    .build();

所以我在 SqliteStudio 中创建了我的小型数据库,现在我正尝试使用 Android 中的 Room 数据库复制它。下面你可以看到我的截图 table users_table from sqliteStudio

依赖项

// Room components
implementation "androidx.room:room-runtime:$rootProject.roomVersion"
implementation 'androidx.wear:wear:1.1.0'
annotationProcessor "androidx.room:room-compiler:$rootProject.roomVersion"
androidTestImplementation "androidx.room:room-testing:$rootProject.roomVersion"

// Lifecycle components
implementation "androidx.lifecycle:lifecycle-viewmodel:$rootProject.lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-livedata:$rootProject.lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-common-java8:$rootProject.lifecycleVersion"

implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'com.google.android.material:material:1.4.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.0'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
compileOnly 'com.google.android.wearable:wearable:2.8.1'
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"

gradle

ext {
    appCompatVersion = '1.3.0'
    constraintLayoutVersion = '2.0.4'
    coreTestingVersion = '2.1.0'
    lifecycleVersion = '2.3.1'
    materialVersion = '1.3.0'
    roomVersion = '2.3.0'
    // testing
    junitVersion = '4.13.2'
    espressoVersion = '3.1.0'
    androidxJunitVersion = '1.1.2'
}

下面您还可以看到来自我的 Dao、DatabaseClass、Entity、Repository、ViewModel 的代码

用户

@Entity(tableName = "users_table")
public class User {

    @PrimaryKey(autoGenerate = true)
    @NonNull
    private int id;

    @ColumnInfo(name = "name")
    @NonNull
    private String name;

    @ColumnInfo(name = "profession")
    @NonNull
    private String profession;

    public User(int id, @NonNull String name, @NonNull String profession) {
        this.id = id;
        this.name = name;
        this.profession = profession;
    }

所有getter和setter也存在

UserDao

@Dao
public interface UserDao {

    @Query("SELECT * FROM users_table")
    LiveData<List<User>> getAll();

    @Insert(onConflict = OnConflictStrategy.IGNORE)
    void insert(User user);

    @Delete
    void delete(User user);

    @Query("DELETE FROM users_table")
    void deleteAll();
}

用户数据库

@Database(entities = {User.class}, version = 1, exportSchema = false)
public abstract class UserDatabase extends RoomDatabase {
    public abstract UserDao userDao();
    private static volatile UserDatabase INSTANCE;

    private static final int NUM_OF_THREADS = 4;


    public static final ExecutorService databaseWriteExecutor
            = Executors.newFixedThreadPool(NUM_OF_THREADS);

    public static UserDatabase getDatabase(final Context context){
        if (INSTANCE == null){
            synchronized (UserDatabase.class){
                if (INSTANCE == null){
                    INSTANCE = Room.databaseBuilder(context.getApplicationContext(),
                            UserDatabase.class, "user.db")
                            .createFromAsset("users.db")
                            .build();
                }
            }
        }
        return INSTANCE;
    }
}

用户资料库

public class UserRepository {


    private UserDao userDao;
    private LiveData<List<User>> allUsers;

    public UserRepository(Application application) {
        UserDatabase db = UserDatabase.getDatabase(application);
        userDao = db.userDao();
        allUsers = userDao.getAll();

    }

    public LiveData<List<User>> getAllData() { return allUsers; }

    public void insert(User user){
        UserDatabase.databaseWriteExecutor.execute(() -> {
            userDao.insert(user);
        });
    }
    public void deleteAll(){
        UserDatabase.databaseWriteExecutor.execute(() -> {
            userDao.deleteAll();
        });
    }
}

UserViewModel

public class UserViewModel extends AndroidViewModel {

    public static UserRepository repository;
    public final LiveData<List<User>> allUsers;


    public UserViewModel(@NonNull Application application) {
        super(application);
        repository = new UserRepository(application);
        allUsers = repository.getAllData();
    }

    public LiveData<List<User>> getAllUsers() { return allUsers; }
    public static void insert(User user) { repository.insert(user); }
}

这只是一个猜测,您正试图在实际访问数据库之前检查数据库是否存在。但是,这是一个相对普遍的问题。它还假定您使用的是兼容设备(例如,对于数据库检查器,必须是 Android API 26+)。

检索数据库实例时,在您的情况下,在 activity/fragment 中使用 UserDatabase mydatabase = UserDatabase.getDatabase(); 时实际上并没有打开数据库,而只是在尝试实际 extract/insert/update/delete 数据库打开的数据,在您的情况下,users.db file/asset 从包中复制到设备上的最终位置。因此,除非尝试访问数据库,否则数据库检查器和设备文件资源管理器都不会显示任何内容。

您可以通过在 UserDatabase class 中的 getDatabase 方法中添加一行来临时(或永久)强制打开。例如你可以使用 :-

public abstract class UserDatabase extends RoomDatabase {
    public abstract UserDao userDao();
    private static volatile UserDatabase INSTANCE;

    private static final int NUM_OF_THREADS = 4;


    public static final ExecutorService databaseWriteExecutor
            = Executors.newFixedThreadPool(NUM_OF_THREADS);

    public static UserDatabase getDatabase(final Context context){
        if (INSTANCE == null){
            synchronized (UserDatabase.class){
                if (INSTANCE == null){
                    INSTANCE = Room.databaseBuilder(context,
                            UserDatabase.class, "user.db")
                            .allowMainThreadQueries() //NOTE ADDED for convenience of demo
                            .createFromAsset("users.db")
                            .build();
                    /*<<<<<<<<<< ADDED to FORCE an open of the database >>>>>>>>>>*/
                    SupportSQLiteDatabase sdb = INSTANCE.getOpenHelper().getWritableDatabase();
                }
            }
        }
        return INSTANCE;
    }
}

演示

下面是演示。 它使用您在问题中发布的代码。但是,进行了以下更改:-

  • Getter 和 Setter 已添加到 User class。
  • Room.databaseBuilder 的上下文使用传递的 context 而不是使用上下文来获取上下文 (这不是问题,因为需要上下文的唯一原因是获取数据库的默认路径).
  • 已添加.allowMainThreadQueries以简化演示(作为整个过程的证明)。

首先使用 SQLite Studio 创建了一个数据库,它填充了 3 行,按照 :-

结构是:-

数据库文件已复制(在SQLite Studio中关闭连接后,再次打开连接然后再次关闭以验证数据库是否正确) (并重命名为 users.db) 到项目的资产文件夹(创建文件夹后),按照 :-

An activity MainActivity 最初用于演示:-

public class MainActivity extends AppCompatActivity {

    UserDatabase db;
    UserDao dao;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        db = UserDatabase.getDatabase(this); // Does not open the database
        dao = db.userDao(); // Does not open the database
    }
}

运行 1

以上是 运行,下面是来自 Android Studio 的结果(注意同时使用设备文件资源管理器和数据库检查器):-

  • 可以看出,在应用成功后,数据库检查器(应用检查)和设备文件资源管理器中都没有显示任何内容运行。

  • NOTE 可以看到getWritableDatabase这一行已经被注释掉了(为了演示不用)

  • 结论仅仅获得一个UserDatabase实例和一个UserDao实例并不会导致数据库被创建。

运行 2

这 运行 表明访问数据库会导致创建数据库。它使用 getAll 查询的非实时数据版本。在 UserDao 中添加了以下内容:-

@Query("SELECT * FROM users_table")
List<User> getAllUsers();

另外 MainActivity 已更改为包含(取消注释)之前注释掉的代码 :-

    for (User user: dao.getAllUsers()) {
        Log.d("USERINFO","User is " + user.getName() + " profession is " + user.getProfession());
    }
  • 即实际访问数据。

现在 运行:-

并且日志包含:-

D/USERINFO: User is Fred profession is Doctor
D/USERINFO: User is Mary profession is Solicitor
D/USERINFO: User is Jane profession is Vet
  • 即数据是根据预填充的数据库。

    • 注意现在有 3 个文件,-wal 和 -shm 是 SQLite 处理的日志文件。
    • 请注意,您应该确保在创建资产时它们不存在。如果他们确实重复 open/close 连接,直到他们不这样做。 createFromAsset 不会(我相信)处理 additional/extra 文件。如果它们确实存在,那么您可能会得到意想不到的结果(例如损坏、丢失数据)

运行 3

为了演示在 getDatabase 方法中强制打开,在 MainActivity 中访问数据库的代码再次按照 *运行 1 注释掉 并且在 UserDatabase 中注释掉的 //SupportSQLiteDatabase sdb = INSTANCE.getOpenHelper().getWritableDatabase(); 被更改为包含在 SupportSQLiteDatabase sdb = INSTANCE.getOpenHelper().getWritableDatabase(); 并且重要 App 已卸载(一旦数据库文件存在,将不会再次从 assets 文件夹中复制,因此删除数据库(卸载 App 即可))

  • 可能删除了数据库文件

没有要复制的资产文件

卸载应用程序并将资产文件重命名为 not_the_users.db 后,结果为 crash/exception 并且日志包括:-

2021-08-16 08:12:11.884 26625-26625/a.a.so68791243javaroomnodatabase E/AndroidRuntime: FATAL EXCEPTION: main
    Process: a.a.so68791243javaroomnodatabase, PID: 26625
    java.lang.RuntimeException: Unable to start activity ComponentInfo{a.a.so68791243javaroomnodatabase/a.a.so68791243javaroomnodatabase.MainActivity}: java.lang.RuntimeException: Unable to copy database file.
  • 所以这消除了无资产文件,如果您正在访问数据库(再次强制打开将确保尽早检测到此类错误)。

运行 4 - 数据库不兼容

为此 运行 users_table table 通过将专业列的类型更改为字符串,作为 users.db 和应用程序复制到资产文件夹中已卸载。结果是预期的 Expected/Found 不匹配 crash/exception 根据:-

2021-08-16 08:30:42.523 27239-27239/a.a.so68791243javaroomnodatabase E/AndroidRuntime: FATAL EXCEPTION: main
    Process: a.a.so68791243javaroomnodatabase, PID: 27239
    java.lang.RuntimeException: Unable to start activity ComponentInfo{a.a.so68791243javaroomnodatabase/a.a.so68791243javaroomnodatabase.MainActivity}: java.lang.IllegalStateException: Pre-packaged database has an invalid schema: users_table(a.a.so68791243javaroomnodatabase.User).
     Expected:
    TableInfo{name='users_table', columns={name=Column{name='name', type='TEXT', affinity='2', notNull=true, primaryKeyPosition=0, defaultValue='null'}, profession=Column{name='profession', type='TEXT', affinity='2', notNull=true, primaryKeyPosition=0, defaultValue='null'}, id=Column{name='id', type='INTEGER', affinity='3', notNull=true, primaryKeyPosition=1, defaultValue='null'}}, foreignKeys=[], indices=[]}
     Found:
    TableInfo{name='users_table', columns={profession=Column{name='profession', type='STRING', affinity='1', notNull=true, primaryKeyPosition=0, defaultValue='null'}, 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'}}, foreignKeys=[], indices=[]}
  • 同样,这消除了预填充数据库的典型问题如果您正在访问数据库。