Android_存儲之DataBase之Room


概述:

 Room是Google在AndroidX中提供的一個ORM(Object Relational Mapping,對象關系映射)庫。它是在SQLite上提供的一個抽象層,可以使用SQLite的全部功能,同時可以更好更便捷流暢地訪問數據庫。(關於AndroidX可以參考,Android_AndroidX簡介&Junit4:https://www.cnblogs.com/fanglongxiang/p/11401123.html

推薦使用Room而不是直接使用SQLite,官方的說明如下:

應用程序處理大量結構化數據 從本地持久化數據中處理是最好的。最常見的用例是緩存相關的數據片段。這樣,當設備無法訪問網絡時,用戶仍然可以在離線時瀏覽該內容。任何用戶發起的內容更改都將在設備重新聯機后同步到服務器。

 

Room的架構和組成:

Room主要由下面3部分組成,這里簡單說明下,后面會詳解。

DataBase:包含數據庫容器,並作為到應用程序的持久關系數據的底層連接的主要訪問點。

    通過注釋@Database實現,並要繼承RoomDatabase。具體見下面例子中的StudentDataBase。

Entity: 實體,相當於數據庫中的表,對應表中的字段。

DAO:  data access objects,簡稱DAO,數據庫訪問對象,包含了訪問數據庫的方法。

應用通過RoomDatabase獲取與之關聯的DAOs,然后通過DAO從數據庫中獲取實體(Entity)並保存相應修改到數據庫中,Entity是用來獲取或設置數據庫的表中對應字段的值。下面是對應的結構圖:

 

下面先舉一個小示例,大致了解下 Room數據庫的增刪改查 流程(代碼中添加了說明注釋,可以詳細看下);然后再詳細說明知識點

要使用Room,首先要加載依賴build.gradle(Module:)添加如下(最新信息從這里獲取 https://developer.android.google.cn/jetpack/androidx/releases/room#declaring_dependencies):

dependencies {
  def room_version = "2.2.0-beta01"

  implementation "androidx.room:room-runtime:$room_version"
  annotationProcessor "androidx.room:room-compiler:$room_version" // For Kotlin use kapt instead of annotationProcessor

  // optional - Kotlin Extensions and Coroutines support for Room
  implementation "androidx.room:room-ktx:$room_version"

  // optional - RxJava support for Room
  implementation "androidx.room:room-rxjava2:$room_version"

  // optional - Guava support for Room, including Optional and ListenableFuture
  implementation "androidx.room:room-guava:$room_version"

  // Test helpers
  testImplementation "androidx.room:room-testing:$room_version"
}

 

首先,創建一個實體Entity,這里創建一個叫Student的實體(它對應一個名叫students的表,表中3個字段id, student_name, age),代碼如下:

package com.flx.testroom;

import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.PrimaryKey;

//@Entity注釋一個實體 tableName 數據庫中的表名
@Entity(tableName = "students")
public class Student {

    //主鍵 自增
    @PrimaryKey(autoGenerate = true)
    public int id;

    //student_name才是真正在表中對應的字段名,name是該對象中的變量。
    @ColumnInfo(name="student_name")
    public String name;

    public int age;

    public Student() {

    }

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

 

然后,創建一個名為StudentsDao的DAO,代碼如下:

package com.flx.testroom;

import java.util.List;

import androidx.room.Dao;
import androidx.room.Delete;
import androidx.room.Insert;
import androidx.room.Query;
import androidx.room.Update;

@Dao public interface StudentsDao {
 @Insert//增 public void insertStudent(Student student);

 @Insert public void insertStudents(Student student1, Student student2);

 @Insert public void insertStudentsAndMore(Student student, List<Student> classmate);

 @Update//改 public void updateStudents(Student ... students);

 @Delete//刪 public void deleStudents(Student ... students);

 @Query( "SELECT * FROM students" )//查
    public Student[] getAllStudents();

 @Query( "SELECT * FROM students WHERE student_name LIKE:studentName" )//帶參數
    public Student[] getStudentsByName(String studentName);

}

 

最后創建數據庫類StudentDataBase,

package com.flx.testroom;

import androidx.room.Database;
import androidx.room.RoomDatabase;

//@Database注釋定義數據庫類, entities指明包含的實體, version表明版本 數據遷移一定需要,后面有講到。 @Database( entities
= {Student.class}, version = 1) public abstract class StudentDataBase extends RoomDatabase { public abstract StudentsDao studentsDao(); }

 

接下來看如何調用,運行后的現象和結果。 這里編寫的是一個運行在Android設備上的JUnit測試。

為了能看到具體數據庫,這里創建數據庫使用持久化的方法,databaseBuilder,下面有詳細說明。

(關於Junit測試可以參考,Android_AndroidX簡介&Junit4:https://www.cnblogs.com/fanglongxiang/p/11401123.html

package com.flx.testroom;

import android.content.Context;
import android.util.Log;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.util.ArrayList;
import java.util.List;

import androidx.room.Room;
import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;

@RunWith(AndroidJUnit4.class)
public class StudentsDaoTest {
    private StudentsDao studentsDao;
    private StudentDataBase studentDb;

    @Before
    public void setUp() throws Exception {
        Context context = InstrumentationRegistry.getTargetContext();
        //studentDB 數據庫的名字
        studentDb = Room.databaseBuilder(context, StudentDataBase.class, "studentDB").build();
//        studentDb = Room.inMemoryDatabaseBuilder( context, StudentDataBase.class ).build();
        studentsDao = studentDb.studentsDao();
    }

    @After
    public void tearDown() throws Exception {
        studentDb.close();
    }

    @Test
    public void StudentsTest() {
     //插入了4組數據,由於id增,這里沒有賦值 List
<Student> listStudents = new ArrayList<>(); Student inStudent1 = new Student("insert1", 20); Student inStudnet2 = new Student( "insert2", 21 ); Student inStudnet3 = new Student( "insert3", 22 ); Student inStudnet4 = new Student( "insert4", 22 ); listStudents.add( inStudnet2 ); listStudents.add( inStudnet3 ); listStudents.add( inStudnet4 ); studentsDao.insertStudentsAndMore( inStudent1, listStudents );     
     //查詢讀取所有數據並打印log, getAllStudents after insert Student[] resultStudents1
= studentsDao.getAllStudents(); for (Student st:resultStudents1 ) { Log.d( "test_flx", "getAllStudents after insert: id = " + st.getId() + "; name = " + st.getName() + "; age = "+st.getAge()); }       
     //更新, 更新了上述查詢的第一組數據 以及 id為2的數據。id為2 這里即第二組 resultStudents1[
0].setName( "update1" ); resultStudents1[0].setAge( 111 ); Student updateStudent = new Student("update2", 222); updateStudent.setId( 2 ); studentsDao.updateStudents( resultStudents1[0], updateStudent );
     //查詢讀取所有數據並打印log,getAllStudents after update Student[] resultStudents2
= studentsDao.getAllStudents(); for (Student st: resultStudents2) { Log.d( "test_flx", "getAllStudents after update: id = " + st.getId() + "; name = " + st.getName() + "; age = "+st.getAge()); }
     //刪除查詢的第一個數據     studentsDao.deleStudents( resultStudents2[
0] );

     //查詢讀取所有數據並打印log,getAllStudents after dele
for (Student st: studentsDao.getAllStudents()) { Log.d( "test_flx", "getAllStudents after dele: id = " + st.getId() + "; name = " + st.getName() + "; age = "+st.getAge()); } } }

@Before->@Test->@After。測試中,創建了叫studentDB的數據庫,具體操作在StudentsTest方法中注釋中詳細說明了,下面看下結果。

 

log打印的結果如下,是符合上述操作的:

2019-07-16 10:35:12.486 32711-32730/com.flx.testroom D/test_flx: getAllStudents after insert: id = 1; name = insert1; age = 20
2019-07-16 10:35:12.486 32711-32730/com.flx.testroom D/test_flx: getAllStudents after insert: id = 2; name = insert2; age = 21
2019-07-16 10:35:12.486 32711-32730/com.flx.testroom D/test_flx: getAllStudents after insert: id = 3; name = insert3; age = 22
2019-07-16 10:35:12.487 32711-32730/com.flx.testroom D/test_flx: getAllStudents after insert: id = 4; name = insert4; age = 22
2019-07-16 10:35:12.502 32711-32730/com.flx.testroom D/test_flx: getAllStudents after update: id = 1; name = update1; age = 111
2019-07-16 10:35:12.502 32711-32730/com.flx.testroom D/test_flx: getAllStudents after update: id = 2; name = update2; age = 222
2019-07-16 10:35:12.502 32711-32730/com.flx.testroom D/test_flx: getAllStudents after update: id = 3; name = insert3; age = 22
2019-07-16 10:35:12.502 32711-32730/com.flx.testroom D/test_flx: getAllStudents after update: id = 4; name = insert4; age = 22
2019-07-16 10:35:12.516 32711-32730/com.flx.testroom D/test_flx: getAllStudents after dele: id = 2; name = update2; age = 222
2019-07-16 10:35:12.517 32711-32730/com.flx.testroom D/test_flx: getAllStudents after dele: id = 3; name = insert3; age = 22
2019-07-16 10:35:12.517 32711-32730/com.flx.testroom D/test_flx: getAllStudents after dele: id = 4; name = insert4; age = 22

  

通過adb進入設備,讀取數據庫數據如下,也是符合的:

 

 

 

通過上面的例子,應該有一點了解或感覺,下面具體說一些Room相關的知識點

 1.  創建Room數據庫 Database:

創建Room數據庫有兩種方式,在上面代碼中測試里 就有選擇了其中一種,持久化的數據庫。

(1)databaseBuilder (Context context, Class<T> klass, String name)

創建的是持久化的數據庫,上面例子就用的這種,生成的數據庫在/data/data/<packagesname>/databases下。

studentDb = Room.databaseBuilder(context, StudentDataBase.class, "studentDB").build();

(2)inMemoryDatabaseBuilder (Context context, Class<T> klass)

內存數據庫,當進程結束后 數據庫也相應的被釋放,數據庫中的信息也丟失了。

studentDb = Room.inMemoryDatabaseBuilder( context, StudentDataBase.class ).build();

  

2. 實體 Entity:

 上述例子中,Student即是實體,可以看代碼中注釋,有部分說明。下面具體說明下。

  • 實體對應數據庫中的表,包含了表的各個字段對應成的字段,以及操作方法(如例子中的每個字段的getter和setter方法)。

注:Android Studio快速生成getter setter方法,可使用Alt+Insert調出如下界面快速構造

 

 

  • 使用實體,首先保證下面的結構組件已添加到build.gradle(Project:)中(這里添加的build.gradle和上面room依賴添加的不是一個文件,注意build.gradle后面的注釋),默認是添加了的。
allprojects {
    repositories {
 google()         jcenter()
        
    }
}
  • @Entity 注釋,定義了一個實體。可以包含多個參數,如下tableName-表名
@Entity(tableName = "students")
public class Student {
}
  • 主鍵使用注釋@PrimaryKey定義,可以是單個字段,也可以是多個字段。如果是多個字段 在@Entity中添加,主鍵要可以設置自增屬性autoGenerate。如:
//多個字段的主鍵
@Entity(primaryKeys = {"AA", "BB"})
public class Student {

}

//主鍵自增
@PrimaryKey(autoGenerate = true)
pulic int id;
  • @ColumnInfo注釋,定義一個變量對應的表中的字段,name屬性即表中字段名。沒有添加的,變量名即表中字段名。
@ColumnInfo(name="student_name")
public String name;
  • 默認實體中每個變量都會在表中新建一列與之對應,如果不想持久化某一個,可以使用變量的注釋@Ignore 或 實體的注釋@Entity(ignoredColumns = "xxxx")進行忽略。
  • FTS(full-text search):全文搜索支持,使用條件是Room版本在2.1.0及以上。通過注釋@Fts3 or @Fts4 定義使用。

注意:1.一般使用@Fts4, 如果應用有嚴格的磁盤要求 或者 要兼容舊版本的SQLite才使用@Fts3。

           2.主鍵必須是 INTEGER類型 名字是rowid.否則會有如下錯誤:

錯誤: The single primary key field in an FTS entity must either be named 'rowid' or must be annotated with @ColumnInfo(name = "rowid")

 簡單例子:

@Fts4 @Entity(tableName = "students")
public class Student {

 @PrimaryKey public int rowid;

    @ColumnInfo(name="student_name")
    public String name;
}

 

  •  索引:通過索引加快查詢,通過unique屬性定義唯一性。如下(包含了兩項的索引,並設置了唯一):
@Entity(tableName = "students",indices = {@Index(value = {"id", "student_name"},
        unique = true)})
public class Student {

    @PrimaryKey
    public int id;

    @ColumnInfo(name="student_name")
    public String name;
}

注意:索引不能在Fts中使用

錯誤: Indices not allowed in FTS Entity.

  

  • 實體類對象間的關系

Room禁止實體類之間的對象引用

大致原因是性能和體驗上的考慮:將關系從數據庫映射到對象上,這種方式再客戶端上存在性能問題。延遲加載(默認方式)是不可行的,它一般發生在UI線程上,UI線程查詢磁盤數據有嚴重性能問題。Ui線程計算 繪制Activity或布局大致要16ms, 即使查詢只要5ms 也可能導致沒有足夠時間,導致視覺上的問題。如果有事務並行或磁盤密集型操作,查詢時間會更久。如果不使用延遲加載,程序獲取的數據比需要的更多,造成內存消耗過多的問題,影響性能。

例如:有兩個對象,Book和Author,每個Book都有一個Author對象。一個UI加載Book列表,可以通過延遲加載 獲取圖書檢索作者的實例。第一次從數據庫中查詢author。一段時間后,可能UI上 還需顯示作者的名字,通過下面代碼添加:

authorNameTextView.setText(book.getAuthor().getName());

這個看起來沒有問題。如果提前查詢作者信息,而如果不再需要數據,就很難更改數據的加載方式。例如,如果您的應用程序的UI不再需要顯示作者信息,應用會繼續加載不再顯示的數據,從而浪費寶貴的內存空間。如果Author類引用其他表(如Books),則應用程序的效率會進一步降低。

  (1). 一對多

下面是另一個名為Book的實體,通過注釋@ForeignKey定義了它與Student的關系。

@Entity(foreignKeys = @ForeignKey(entity = Student.class,
        parentColumns = "id",
        childColumns = "student_id",
        onDelete = ForeignKey.CASCADE))//onDelete = ForeignKey.CASCADE,當Student被刪除,則所有與之關聯的Book都會被刪除。
public class Book {
    @PrimaryKey
    public int bookId;

    public String title;

    @ColumnInfo(name = "student_id")
    public int studentId;
}

 這里的一對多,是Student對Book。通過外鍵student_id使多個Book實例 關聯到 Student實例上。

 

  (2). 嵌套對象

如下在Student中嵌套了一個Address對象,這樣Student實體對象就對應包含了這些列:id, student_name, street, state, city, post_code。

public class Address {
    public String street;
    public String state;
    public String city;

    @ColumnInfo(name = "post_code")
    public int postCode;
}


@Entity(tableName = "students",indices = {@Index(value = {"id", "student_name"},
        unique = true)})
public class Student {

    @PrimaryKey
    public int id;

    @ColumnInfo(name="student_name")
    public String name;

    @Embedded public Address address;
}

 

  (3). 多對多

多對多,通過下面例子(官網)來具體看看。下面包含了兩個實體,Playlist 和Song,以及一個中間類 PlaylistSongJoin 保存每個播放列表中的歌曲信息。

@Entity
public class Playlist {
    @PrimaryKey public int id;

    public String name;
    public String description;
}

@Entity
public class Song {
    @PrimaryKey public int id;

    public String songName;
    public String artistName;
}

  PlaylistSongJoin:

@Entity(tableName = "playlist_song_join",
        primaryKeys = { "playlistId", "songId" },
        foreignKeys = {
                @ForeignKey(entity = Playlist.class,
                            parentColumns = "id",
                            childColumns = "playlistId"),
                @ForeignKey(entity = Song.class,
                            parentColumns = "id",
                            childColumns = "songId")
                })
public class PlaylistSongJoin {
    public int playlistId;
    public int songId;
}

 

3. 數據庫視圖

2.1.0及更高版本才支持,和實體Entity類似,可以對視圖執行SELECT,但不能執行INSERT, UPDATE, DELETE。

創建視圖:使用注釋@DatabaseView,注釋的值設置成查詢。

@DatabaseView("SELECT user.id, user.name, user.departmentId," +
              "department.name AS departmentName FROM user " +
              "INNER JOIN department ON user.departmentId = department.id")
public class UserDetail {
    public long id;
    public String name;
    public long departmentId;
    public String departmentName;
}

 

 關聯視圖到數據庫

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

 

4. DAO 訪問數據庫

 要使用Room持久化庫,需要通過數據訪問對象DAO(data access objects)。 它可以是一個接口,也可以是一個抽象類。

Room不支持在主線程訪問數據庫(在構造數據庫時若調用 allowMainThreadQueries() 可繞過),因為它可能比較長時間鎖定UI(主線程) 導致ANR。若要在主線程訪問數據庫,需要使用異步方法 或者  手動將調用移到后台線程。

最開始的例子 中的StudentsDao就是這樣的類。再次給出 :

@Dao
public interface StudentsDao {
    @Insert public void insertStudent(Student student);

    @Insert
    public void insertStudents(Student student1, Student student2);

    @Insert
    public void insertStudentsAndMore(Student student, List<Student> classmate);

    @Update public void updateStudents(Student ... students);

    @Delete public void deleStudents(Student ... students);

    @Query( "SELECT * FROM students" )
    public Student[] getAllStudents();

    @Query( "SELECT * FROM students WHERE student_name LIKE:studentName" )
    public Student[] getStudentsByName(String studentName);

    @Query("SELECT student_name, age FROM students")
    public List<Student> getNameAndAge();
}

 增刪改查 通過注釋@Insert  @Delete @Update @Query使用即可,Room已生成對應的實現。其中@Update @Delete是通過主鍵找到對應實體再進行操作。@Query使用SQL語句,返回對應的集合。

上面列了幾種插入和查詢的情況。

在上面介紹 實體對象間關系 中的 多對多 時,舉的例子 兩個實體,Playlist 和Song,以及一個中間類 PlaylistSongJoin ,下面是通過song查詢playlist 和 通過playlist查詢songs的代碼,可以了解下:

@Dao
public interface PlaylistSongJoinDao {
    @Insert
    void insert(PlaylistSongJoin playlistSongJoin);

    @Query("SELECT * FROM playlist " +
           "INNER JOIN playlist_song_join " +
           "ON playlist.id=playlist_song_join.playlistId " +
           "WHERE playlist_song_join.songId=:songId")
    List<Playlist> getPlaylistsForSong(final int songId);

    @Query("SELECT * FROM song " +
           "INNER JOIN playlist_song_join " +
           "ON song.id=playlist_song_join.songId " +
           "WHERE playlist_song_join.playlistId=:playlistId")
    List<Song> getSongsForPlaylist(final int playlistId);
}

 

5. Room數據庫 遷移

當應用添加或改變某些東西,需要修改實體Entity時,對應的數據庫結構也會改變。這時需要保存已有的數據,進行數據庫遷移。

數據庫遷移,通過實現Migration這個遷移類,這個類構造指明了startVersion和endVersion(數據庫類的@Database中添加的version值), 實現了migrate方法。如下:

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`))");
    }
};

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");
    }
};

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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM