编程知识 cdmana.com

Room of Android jetpack architecture component (6)

One 、Room brief introduction

stay Android Application development , There are many ways to persist data , Common are Shared Preferences、Internal Storage、External Storage、SQLite Databases and Network Connection Five kinds . among ,SQLite Use database for storage , It is suitable for storing large amount of data .

however , because SQLite Writing is tedious and error prone , therefore , There are all kinds of ORM(Object Relational Mapping) library , Such as ORMLite、Realm、LiteOrm and GreenDao etc. , These third-party repositories have a common purpose , That's for the convenience of developers ORM And it appears , Simplified operations include creating 、 upgrade 、CRUD And so on .
 Insert picture description here

In order to simplify the SQLite operation ,Jetpack The library provides Room Components , Used to help developers simplify the operation of the database .Room The persistence library provides a SQLite Abstraction layer , Let developers access the database more robust , The performance of database operations has also been improved .

Two 、Room Use

2.1 Room Relevant concepts

Room Component library contains 3 Important concepts , Distribution is Entity、Dao and Database.

  • Entity: Entity class , It corresponds to a table structure of the database , You need to use annotations @Entity marked .
  • Dao: Contains access to a series of ways to access the database , You need to use annotations @Dao marked .
  • Database: Database holder , It is the main access point for the underlying connection of application persistence related data , You need to use annotations @Database marked .

Use @Database The following conditions shall be met for annotation :

  • The defined class must be a class inherited from RoomDatabase The abstract class of .
  • In the annotation, you need to define the list of entity classes associated with the database .
  • Contains an abstract method with no arguments and returns an annotated @Dao.

Simply speaking , Application and use Room Database to get the data access objects associated with the database (DAO). Then the app uses each DAO Get entities from the database , All changes to these entities are then saved back to the database . Finally, the application uses entities to get and set the values corresponding to the table columns in the database .

Here's how to use Entity、Dao、Database Three and the corresponding architecture of the application , As shown below .
 Insert picture description here

2.2 Basic use

2.2.1 Add dependency

First , stay app Of build.gradle Add the following configuration script .

dependencies {
    
    def room_version = "2.2.5"
    implementation "androidx.room:room-runtime:$room_version"
    annotationProcessor "androidx.room:room-compiler:$room_version"
}

2.2.2 Entity

Room The use of traditional Sqlite The process of using database is similar . First , Use @Entity Annotations define an entity class , Class is mapped to a table in the database , The class name of the default entity class is table name , The field name is table name , As shown below .

@Entity
public class User {
    @PrimaryKey(autoGenerate = true)
    public int uid;

    @ColumnInfo(name = "first_name")
    public String firstName;

    @ColumnInfo(name = "last_name")
    public String lastName;

    @Ignore
    public boolean sex;
}

among ,@PrimaryKey Annotations are used to mark the primary key of a table , And use autoGenerate = true To specify the primary key self growth .@ColumnInfo Annotation is used to mark the information of the corresponding column of the table, such as the table name 、 Default value and so on .@Ignore Annotations are used to indicate that this field is ignored , Fields with this annotation will not generate corresponding column information in the database .

2.2.3 Dao

Dao Class is an interface , It is mainly used to define a series of methods to operate the database , That is, what we usually call adding, deleting, modifying and checking . In order to facilitate developers to operate the database ,Room Provides @Insert、@Delete、@Update and @Query Etc .

@query annotation
@Query It's a query comment , Its parameters are String type , Let's write directly SQL Statement to execute . such as , We according to the ID Query a user's information .

@Query("SELECT * FROM user WHERE uid IN (:userIds)")
List<User> loadAllByIds(int[] userIds);

@Insert annotation
@Insert Annotations are used to insert a piece of data into a table , We define a method and use @Insert Just annotate , As shown below .

@Insert
void insertAll(User... users);

among ,@Insert There's a note onConflict Parameters , Represents the processing logic when the inserted data already exists , There are three kinds of operation logic , Distribution is REPLACE、ABORT and IGNORE.

@Delete annotation
@Delete Annotations are used to delete table data , As shown below .

@Delete
void delete(User user);

@Update annotation
@Update Annotations are used to modify a piece of data , and @Delete It is also based on the primary key to find the entity to be deleted .

@Update
void update(User user);

Next , We build a new one UserDao class , And add the following code .

@Dao
public interface UserDao {
    @Query("SELECT * FROM user")
    List<User> getAll();

    @Query("SELECT * FROM user WHERE uid IN (:userIds)")
    List<User> loadAllByIds(int[] userIds);

    @Query("SELECT * FROM user WHERE first_name LIKE :first AND last_name LIKE :last LIMIT 1")
    User findByName(String first, String last);

    @Insert
    void insertAll(User... users);

    @Delete
    void delete(User user);

    @Query("DELETE  FROM user WHERE uid = :uid ")
    void deleteUserById(int uid);

    @Query("UPDATE  user SET first_name = :firstName where uid =  :uid")
    void updateUserById(int uid, String firstName);

    @Update
    void update(User user);
}

2.2.4 Database

First , Define an inheritance RoomDatabase The abstract class of , And use @Database Note to identify , As shown below .

@Database(entities = {User.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
    public abstract UserDao userDao();

    private static  AppDatabase instance = null;

    public static synchronized AppDatabase getInstance(Context context) {
        if (instance == null) {
            instance = Room.databaseBuilder(
                    context.getApplicationContext(),
                    AppDatabase.class,
                    "user.db" // Database name 
                ).allowMainThreadQueries().build();
        }
        return instance;
    }
}

After completing the above operations , Use the following code to get an instance of creating a database .

    AppDatabase db = AppDatabase.getInstance(this);
        UserDao dao = db.userDao();
        User user=new User();
        user.firstName="ma";
        user.lastName="jack";
        dao.insertAll(user); 

2.2.5 Comprehensive examples

Next , Let's talk about Room The basic use method . First , We are activity_main.xml New in layout file 4 Button , They are used to add, delete, modify and search .

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:gravity="center"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/btn_insert"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAllCaps="false"
        android:textSize="24dp"
        android:text=" insert data " />

    <Button
        android:id="@+id/btn_delete"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:textSize="24dp"
        android:text=" Delete data " />

    <Button
        android:id="@+id/btn_query"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:textSize="24dp"
        android:text=" Query data " />

    <Button
        android:id="@+id/btn_update"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:textSize="24dp"
        android:text=" Update data " />

</LinearLayout>

Then we write code to implement the related functions , As shown below .

public class MainActivity extends AppCompatActivity {

    AppDatabase db=null;
    UserDao dao=null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        init();
    }

    private void init() {
       db = AppDatabase.getInstance(this);
       dao = db.userDao();
       insert();
       query();
       update();
    }


    private void insert() {
        findViewById(R.id.btn_insert).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                for (int i=0;i<10;i++) {
                    User user=new User(" Zhang San "+i,"100"+i);
                    dao.insertAll(user);
                }
            }
        });
    }

    private void query() {
        findViewById(R.id.btn_query).setOnClickListener(new View.OnClickListener() {
            @RequiresApi(api = Build.VERSION_CODES.N)
            @Override
            public void onClick(View v) {
                dao.getAll().forEach(new Consumer<User>() {
                    @Override
                    public void accept(User user) {
                        Log.d("Room", user.firstName+","+user.lastName);
                    }
                });
            }
        });
    }

    private void update() {
        findViewById(R.id.btn_update).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                dao.updateUserById(2, " Li Si ");
                User updateUser = dao.loadUserById(2);
                Log.e("Room", "update${user.firstName},${user.lastName}");
            }
        });
    }
}

Next , Run code , Perform the insert operation , The generated database is located in data/data/packageName/databases Under the table of contents . then , Then execute the query function , The output of the console is as follows .

com.xzh.jetpack D/Room:  Zhang San 0,1000
com.xzh.jetpack D/Room:  Zhang San 1,1001
com.xzh.jetpack D/Room:  Zhang San 2,1002
com.xzh.jetpack D/Room:  Zhang San 3,1003
com.xzh.jetpack D/Room:  Zhang San 4,1004
com.xzh.jetpack D/Room:  Zhang San 5,1005
com.xzh.jetpack D/Room:  Zhang San 6,1006
com.xzh.jetpack D/Room:  Zhang San 7,1007
com.xzh.jetpack D/Room:  Zhang San 8,1008
com.xzh.jetpack D/Room:  Zhang San 9,1009

It should be noted that , All operations on the database cannot be performed in the main thread , Except in the database Builder It's up-regulated allowMainThreadQueries() Or all operations are done in a child thread , Otherwise, the program will crash and report the following error .

java.lang.IllegalStateException: Cannot access database on the main thread since it may potentially lock the UI for a long period of time.

3、 ... and 、 Pre population database

occasionally , We want to have a specific set of data loaded in the database when the application starts , We call this behavior prefill databases . stay Room 2.2.0 And later , Developers can use API Method to pre fill with the contents of the prepackaged database file in the device file system during initialization Room database .

3.1 Pre fill from application resources

Prefill refers to the application from assets/ The database files in any location in the directory are pre filled Room database , Call when using createFromAsset() Method , Then call build() The method can , As shown below .

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

createFromAsset() Method accepts a method that contains assets/ The string parameter of the relative path of the directory .

3.2 Prefill from the file system

In addition to building data into applications assets/ Except for the catalogue , We can still Read the prepackaged database file from any location on the device file system to pre fill Room database , You need to call createFromFile() Method , Then call build(), As shown below .

    Room.databaseBuilder(appContext, AppDatabase.class, "Sample.db")
        .createFromFile(new File("mypath"))
        .build();
    

createFromFile() Method to accept... Representing the absolute path of the prepackaged database file File Parameters ,Room Creates a copy of the specified file , Instead of just opening it up , And when using, please make sure that the application has the read permission of the file .

Four 、 Migrate database

4.1 Basic use

When using the database, it is inevitable to upgrade the database . for example , As the business changes , You need to add a field to the data table , At this point, you need to upgrade the data table .

stay Room in , Database upgrade or downgrade needs to use Migration class . Every Migration Subclasses are replaced by Migration.migrate() Method definition startVersion and endVersion The migration path between . When the application update needs to upgrade the database version ,Room From one or more Migration Subclasses run migrate() Method , To migrate the database to the latest version at run time .

for example , The database version applied in the current device is 1, If you want to change the version of the database from 1 Upgrade to 2, So here's the code .

static final Migration MIGRATION_1_2 = new Migration(1, 2) {
    @Override
    public void migrate(SupportSQLiteDatabase database) {
        database.execSQL("CREATE TABLE `Fruit` (`id` INTEGER, "
                + "`name` TEXT, PRIMARY KEY(`id`))");
    }
};

among ,Migration Method needs startVersion and endVersion Two parameters ,startVersion Represents the version where the upgrade started ,endVersion Indicates the version to upgrade to , At the same time, we need to @Database In the annotations version The value of is changed to and endVersion identical .

And so on , If the database version of the current application is 2, Want to upgrade to version 3, So here's the code .

static final Migration MIGRATION_2_3 = new Migration(2, 3) {
    @Override
    public void migrate(SupportSQLiteDatabase database) {
        database.execSQL("ALTER TABLE Book "
                + " ADD COLUMN pub_year INTEGER");
    }
};

stay Migration After writing the upgrade plan , You also need to use addMigrations() Method to add the upgraded scheme to Room in , As shown below .

Room.databaseBuilder(getApplicationContext(), MyDb.class, "database-name")
        .addMigrations(MIGRATION_1_2, MIGRATION_2_3).build();

Here is the complete code .

    static final Migration MIGRATION_1_2 = new Migration(1, 2) {
        @Override
        public void migrate(SupportSQLiteDatabase database) {
//            database.execSQL(""); perform sql sentence 
        }
    };

    static final Migration MIGRATION_2_3 = new Migration(2, 3) {
        @Override
        public void migrate(SupportSQLiteDatabase database) {
//            database.execSQL(""); perform sql sentence 
        }
    };
    
Room.databaseBuilder(app,AppDatabase.class, DB_NAME)
    .addMigrations(MIGRATION_1_2, MIGRATION_2_3)
    .build();    

then , stay Android Studio Click on the toolbar of 【View】->【Tool windows】->【Device File Explorer】 Open the data table to see .

The steps of database degradation and upgrade are similar , Is also used addMigrations It's just startVersion > endVersion . When there is a version mismatch in the upgrade or downgrade process , By default, exceptions will be thrown directly .

Of course, we can also handle exceptions . When upgrading, you can add fallbackToDestructiveMigration Method , When the version is not matched, the table will be deleted and recreated . Add... When demoting fallbackToDestructiveMigrationOnDowngrade Method , When the version is not matched, the table will be deleted and recreated .

4.2 Migration test

Migration is often complex , And database migration errors may cause the application to crash . In order to maintain the stability of the application , Developers need to test the migration . So ,Room Provides a room-testing To help complete this testing process .

4.2.1 Export schema

Room You can export the schema information of the database at compile time as JSON file . To export the schema , Please be there. app/build.gradle Set in file room.schemaLocation Comment processor properties , As shown below .

android {
    ...
    defaultConfig {
        ...
        javaCompileOptions {
            annotationProcessorOptions {
                arguments += ["room.schemaLocation":
                             "$projectDir/schemas".toString()]
            }
        }
    }
}

Derived JSON The file represents the schema history of the database . You should store these files in the version control system , Because the system allows Room Create an older version of the database for testing purposes .

4.2.2 Testing a single migration

Before testing migration , You need to add test dependencies first androidx.room:room-testing, And add the location of the exported schema as the resource directory , As shown below .

android {
    ...
    sourceSets {
        // Adds exported schema location as test app assets.
        androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
    }
}

dependencies {
    ...
      testImplementation "androidx.room:room-testing:2.2.5"
}

The test package provides... That can read the exported schema file MigrationTestHelper class . The software package also implements JUnit4 TestRule Interface , So you can manage the created database . for example , Here is the sample code for a single migration test .

@RunWith(AndroidJUnit4.class)
public class MigrationTest {
    private static final String TEST_DB = "migration-test";

    @Rule
    public MigrationTestHelper helper;

    public MigrationTest() {
        helper = new MigrationTestHelper(InstrumentationRegistry.getInstrumentation(),
                MigrationDb.class.getCanonicalName(),
                new FrameworkSQLiteOpenHelperFactory());
    }

    @Test
    public void migrate1To2() throws IOException {
        SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 1);

        // db has schema version 1. insert some data using SQL queries.
        // You cannot use DAO classes because they expect the latest schema.
        db.execSQL(...);

        // Prepare for the next version.
        db.close();

        // Re-open the database with version 2 and provide
        // MIGRATION_1_2 as the migration process.
        db = helper.runMigrationsAndValidate(TEST_DB, 2, true, MIGRATION_1_2);

        // MigrationTestHelper automatically verifies the schema changes,
        // but you need to validate that the data was migrated properly.
    }
}

4.2.3 Test all migrations

Although you can test a single incremental migration , But it is recommended that you add a test , Covers all migrations defined for the database of the application . This ensures that there is no difference between the recently created database instance and the old instance following the defined migration path . The following example demonstrates migrating all tests .

@RunWith(AndroidJUnit4.class)
public class MigrationTest {
    private static final String TEST_DB = "migration-test";

    @Rule
    public MigrationTestHelper helper;

    public MigrationTest() {
        helper = new MigrationTestHelper(InstrumentationRegistry.getInstrumentation(),
                AppDatabase.class.getCanonicalName(),
                new FrameworkSQLiteOpenHelperFactory());
    }

    @Test
    public void migrateAll() throws IOException {
        // Create earliest version of the database.
        SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 1);
        db.close();

        // Open latest version of the database. Room will validate the schema
        // once all migrations execute.
        AppDatabase appDb = Room.databaseBuilder(
                InstrumentationRegistry.getInstrumentation().getTargetContext(),
                AppDatabase.class,
                TEST_DB)
                .addMigrations(ALL_MIGRATIONS).build();
        appDb.getOpenHelper().getWritableDatabase();
        appDb.close();
    }

    // Array of all migrations
    private static final Migration[] ALL_MIGRATIONS = new Migration[]{
            MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4};
}

4.3 Migration exception handling

In the process of migrating the database , Inevitably, there will be some anomalies , If Room Unable to find migration path to upgrade existing database on device to current version , Will prompt IllegalStateException error . In the absence of a migration path , If you lose existing data, it's acceptable , So when you create a database, you can call fallbackToDestructiveMigration() Builder methods .

Room.databaseBuilder(getApplicationContext(), MyDb.class, "database-name")
        .fallbackToDestructiveMigration()
        .build();

fallbackToDestructiveMigration() The method will indicate Room When you need to perform an incremental migration without a defined migration path , Destructively recreate the application's database tables . If you just want Room In certain cases, back to destructive re creation , have access to fallbackToDestructiveMigration() Some of the alternatives to , As shown below .

  • fallbackToDestructiveMigrationFrom(): This method can be used if a particular version of the architecture history causes an unresolved problem in the migration path .
  • fallbackToDestructiveMigrationOnDowngrade(): If you want to... Only when migrating from a higher database version to a lower database version Room You can use this method to go back to destructive re creation .

5、 ... and 、 Test and debug the database

5.1 Test database

To test the database we created , Sometimes you need to Activity Write some test code in . stay Android There are two ways to test a database in .

  • stay Android Test on the device .
  • Test on host development computer ( Not recommended ).

5.1.1 stay Android Test the database on the device

To test the database implementation , The recommended method is to write it in Android Running on the device JUnit test , Since these tests do not need to be created Activity, So they should be executed faster than interface testing . Here is a JUnit Examples of testing .

@RunWith(AndroidJUnit4.class)
public class SimpleEntityReadWriteTest {
    private UserDao userDao;
    private TestDatabase db;

    @Before
    public void createDb() {
        Context context = ApplicationProvider.getApplicationContext();
        db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class).build();
        userDao = db.getUserDao();
    }

    @After
    public void closeDb() throws IOException {
        db.close();
    }

    @Test
    public void writeUserAndReadInList() throws Exception {
        User user = TestUtil.createUser(3);
        user.setName("george");
        userDao.insert(user);
        List<User> byName = userDao.findUsersByName("george");
        assertThat(byName.get(0), equalTo(user));
    }
}

5.1.2 Test the database on the host

Room Use SQLite support library , The support library provides support for Android The interface corresponding to the interface in the framework class . With this support , Developers can pass the custom implementation of the support library to test the database query .

5.2 Debug database

Android SDK Contains a sqlite3 Database tools , The database that can be used to check the application . It contains .dump And for outputting existing tables SQL CREATE Of the statement .schema Wait for the order . We can execute it at the command line SQLite command , As shown below .

adb -s emulator-5554 shell
sqlite3 /data/data/your-app-package/databases/rssitems.db

added sqlite3 The command line can refer to SQLite On the website sqlite3 Command line documentation .

版权声明
本文为[xiangzhihong]所创,转载请带上原文链接,感谢
https://cdmana.com/2020/12/20201224102527404T.html

Scroll to Top