天天看點

Jetpack系列之Room----入門(二)定義對象之間的關系編寫異步DAO查詢Create views into a database預先填充您的Room database

翻譯自android官網,可直接去官網觀看

Jetpack系列之Room----入門(二)

  • 定義對象之間的關系
    • Create embedded objects
    • Define one-to-one relationships
    • Define one-to-many relationships
    • Define many-to-many relationships
    • Define nested relationships
  • 編寫異步DAO查詢
    • Language and framework options
      • Kotlin with Flow and couroutines
      • Java with RxJava
      • Java與LiveData和Guava
    • Write asynchronous one-shot queries
    • Write observable queries
  • Create views into a database
    • Create a view
    • Associate a view with your database
  • 預先填充您的Room database
    • Prepopulate from an app asset
    • Prepopulate from the file system
    • Handle migrations that include prepackaged databases
      • Example: Fallback migration with a prepackaged database
      • Example: Implemented migration with a prepackaged database
      • Example: Multi-step migration with a prepackaged database

定義對象之間的關系

由于SQLite是關系資料庫,是以您可以指定實體之間的關系。即使大多數對象關系映射庫都允許實體對象互相引用,但Room明确禁止這樣做。要了解此決策背後的技術推理,請參閱了解Room為什麼不允許對象引用。

Create embedded objects

建立嵌入式對象

有時,您希望在資料庫邏輯中将實體或資料對象表示為内聚的整體,即使對象包含多個字段也是如此。在這種情況下,您可以使用 @Embedded 批注表示要分解為表中其子字段的對象。然後,您可以查詢嵌入字段,就像查詢其他各個列一樣。

例如,你的User類可以包括Address類型的字段,它代表的命名字段組成street,city,state,和 postCode。要将組成的列分别存儲在表中,請在User類中用一個@Embedded注釋 Address字段,如以下代碼片段所示:

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

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

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

    public String firstName;

    @Embedded public Address address;
}
           

表示該表User對象則包含以下名稱的列:id,firstName,street,state,city,和post_code。

注意:嵌入字段也可以包括其他嵌入字段。

如果一個實體具有多個相同類型的嵌入字段,則可以通過設定該prefix 屬性使每一列保持唯一 。然後,Room将提供的值添加到嵌入對象中每個列名稱的開頭。

Define one-to-one relationships

定義一對一關系

一對一的關系兩個實體之間是有關系的,其中父實體的每個執行個體正好對應子實體的一個執行個體,反之亦然。

例如,考慮一個音樂流應用程式,其中使用者擁有他們擁有的歌曲庫。每個使用者隻有一個庫,每個庫恰好對應一個使用者。是以,User實體與Library實體之間應該存在一對一的關系。

首先,為您的兩個實體中的每個實體建立一個類。實體之一必須包含一個變量,該變量是對另一個實體的主鍵的引用。

@Entity
public class User {
    @PrimaryKey public long userId;
    public String name;
    public int age;
}

@Entity
public class Library {
    @PrimaryKey public long libraryId;
    public long userOwnerId;
}
           

為了查詢使用者清單和相應的庫,必須首先對兩個實體之間的一對一關系模組化。為此,請建立一個新的資料類,其中每個執行個體都包含父實體的執行個體和子實體的對應執行個體。将@Relation注釋添加到子實體的執行個體中,将parentColumn設定為父實體的主鍵列的名稱,将entityColumn設定為引用父實體主鍵的子實體的列的名稱。

public class UserAndLibrary {
    @Embedded public User user;
    @Relation(
         parentColumn = "userId",
         entityColumn = "userOwnerId"
    )
    public Library library;
}
           

最後,向DAO類添加一個方法,該方法傳回将父實體和子實體配對的資料類的所有執行個體。此方法需要Room運作兩個查詢,是以請向該方法添加注釋@Transaction,以確定整個操作是原子執行的。

@Transaction
@Query("SELECT * FROM User")
public List<UserAndLibrary> getUsersAndLibraries();
           

Define one-to-many relationships

兩個實體之間的一對多關系是指父實體的每個執行個體對應于子實體的零個或多個執行個體,但子實體的每個執行個體隻能對應父實體的一個執行個體。

在音樂流應用程式示例中,假設使用者具有将其歌曲整理到播放清單中的能力。每個使用者可以根據需要建立任意數量的播放清單,但是每個播放清單僅由一個使用者建立。是以,User實體與Playlist實體之間應該存在一對多的關系。

首先,為您的兩個實體中的每個實體建立一個類。與前面的示例一樣,子實體必須包括一個變量,該變量是對父實體主鍵的引用。

@Entity
public class User {
    @PrimaryKey public long userId;
    public String name;
    public int age;
}

@Entity
public class Playlist {
    @PrimaryKey public long playlistId;
    public long userCreatorId;
    public String playlistName;
}
           

為了查詢使用者清單和相應的播放清單,必須首先對兩個實體之間的一對多關系模組化。為此,請建立一個新的資料類,其中每個執行個體都包含父實體的執行個體和所有對應的子實體執行個體的清單。将@Relation 注釋添加到子實體的執行個體,parentColumn設定為父實體的主鍵列的名稱,并将entityColumn 設定為引用父實體的主鍵的子實體的列的名稱。

public class UserWithPlaylists {
    @Embedded public User user;
    @Relation(
         parentColumn = "userId",
         entityColumn = "userCreatorId"
    )
    public List<Playlist> playlists;
}
           

最後,向DAO類添加一個方法,該方法傳回将父實體和子實體配對的資料類的所有執行個體。此方法需要Room運作兩個查詢,是以請向該方法添加注釋@Transaction,以確定整個操作是原子執行的。

@Transaction
@Query("SELECT * FROM User")
public List<UserWithPlaylists> getUsersWithPlaylists();
           

Define many-to-many relationships

兩個實體之間的多對多關系是指父實體的每個執行個體對應于子實體的零個或多個執行個體,反之亦然。

在音樂流應用程式示例中,請再次考慮使用者定義的播放清單。每個播放清單可以包括許多歌曲,并且每首歌曲可以是許多不同播放清單的一部分。是以,Playlist實體與Song實體之間應該存在多對多關系。

首先,為您的兩個實體中的每個實體建立一個類。多對多關系不同于其他關系類型,因為在子實體中通常沒有引用父實體。而是,建立一個第三類來表示兩個實體之間的關聯實體(或交叉引用表)。交叉引用表必須具有表中表示的多對多關系中每個實體的主鍵列。在該示例中,交叉引用表中的每一行對應于一個Playlist執行個體和一個Song執行個體的配對,在該執行個體中,所引用的歌曲被包括在所引用的播放清單中。

@Entity
public class Playlist {
    @PrimaryKey public long playlistId;
    public String playlistName;
}

@Entity
public class Song {
    @PrimaryKey public long songId;
    public String songName;
    public String artist;
}

@Entity(primaryKeys = {"playlistId", "songId"})
public class PlaylistSongCrossRef {
    public long playlistId;
    public long songId;
}
           

下一步取決于您要如何查詢這些相關實體。

  • 如果要查詢playlists和每個playlist對應的song的清單,請建立一個新的資料類,該類包含單獨的playlist和該playlist包含的所有song對象的list 。
  • 如果要查詢songs 及其對應的playlists 對象的list ,請建立一個新的資料類,該類包含 Song 對象和包含該Song的所有Playlist對象的list 。

在這兩種情況下,都可以通過使用這些類中每個注釋中的associateBy屬性來模組化實體之間的關系, @Relation以辨別提供Playlist實體與Song實體之間關系的交叉引用實體。

public class PlaylistWithSongs {
    @Embedded public Playlist playlist;
    @Relation(
         parentColumn = "playlistId",
         entityColumn = "songId",
         associateBy = @Junction(PlaylistSongCrossref.class)
    )
    public List<Song> songs;
}

public class SongWithPlaylists {
    @Embedded public Song song;
    @Relation(
         parentColumn = "songId",
         entityColumn = "playlistId",
         associateBy = @Junction(PlaylistSongCrossref.class)
    )
    public List<Playlist> playlists;
}
           

最後,向DAO類中添加一個方法以公開您的應用程式所需的查詢功能。

  • getPlaylistsWithSongs:此方法查詢資料庫并傳回所有結果PlaylistWithSongs對象。
  • getSongsWithPlaylists:此方法查詢資料庫并傳回所有結果SongWithPlaylists對象。

這些方法每個都需要Room運作兩個查詢,是以請@Transaction向這兩個方法添加 注釋,以確定整個操作是原子執行的。

@Transaction
@Query("SELECT * FROM Playlist")
public List<PlaylistWithSongs> getPlaylistsWithSongs();

@Transaction
@Query("SELECT * FROM Song")
public List<SongWithPlaylists> getSongsWithPlaylists();
           

注意:如果@Relation注釋不符合您的特定用例,則可能需要JOIN在SQL查詢中使用關鍵字來手動定義适當的關系。要了解有關手動查詢多個表的更多資訊,請閱讀使用Room DAO通路資料。

Define nested relationships

定義嵌套關系

有時,您可能需要查詢三個或更多彼此相關的表。在這種情況下,您将在表之間定義嵌套關系。

假設在音樂流應用程式示例中,您要查詢所有使用者,每個使用者的所有播放清單以及每個使用者的每個播放清單中的所有歌曲。使用者與播放清單具有一對多關系,而播放清單與歌曲具有多對多關系。下面的代碼示例顯示了表示這些實體的類,以及播放清單和歌曲之間多對多關系的交叉引用表:

@Entity
public class User {
    @PrimaryKey public long userId;
    public String name;
    public int age;
}

@Entity
public class Playlist {
    @PrimaryKey public long playlistId;
    public long userCreatorId;
    public String playlistName;
}
@Entity
public class Song {
    @PrimaryKey public long songId;
    public String songName;
    public String artist;
}

@Entity(primaryKeys = {"playlistId", "songId"})
public class PlaylistSongCrossRef {
    public long playlistId;
    public long songId;
}
           

首先,像往常一樣,使用資料類和@Relation批注對集合中兩個表之間的關系進行模組化 。以下示例顯示了PlaylistWithSongs一個對Playlist實體類和Song實體類之間的多對多關系進行模組化的類:

public class PlaylistWithSongs {
    @Embedded public Playlist playlist;
    @Relation(
         parentColumn = "playlistId",
         entityColumn = "songId",
         associateBy = @Junction(PlaylistSongCrossRef.class)
    )
    public List<Song> songs;
}
           

定義表示該關系的資料類後,建立另一個資料類,以對集合中的另一個表與第一個關系類之間的關系進行模組化,“嵌套”新表中的現有關系。下面的示例顯示一個UserWithPlaylistsAndSongs類,該類為User實體類和 PlaylistWithSongs關系類之間的一對多關系模組化:

public class UserWithPlaylistsAndSongs {
    @Embedded public User user;
    @Relation(
        entity = Playlist.class,
        parentColumn = "userId",
        entityColumn = "userCreatorId"
    )
    public List<PlaylistWithSongs> playlists;
}
           

userWithPlaylistandSongs類間接地模組化了三個實體類之間的關系:User,Playlist和Song。如圖1所示。

Jetpack系列之Room----入門(二)定義對象之間的關系編寫異步DAO查詢Create views into a database預先填充您的Room database

圖1.音樂流應用程式示例中的關系類圖。

UserWithPlaylistsAndSongs對User和PlaylistWithSongs之間的關系進行模組化,而後者又對Playlist和Song之間的關系進行模組化。

如果您的集合中還有其他表,則建立一個類以對其餘每個表之間的關系進行模組化,并為對所有先前表之間的關系進行模組化的關系類進行模組化。這将在您要查詢的所有表之間建立一串嵌套關系。

最後,向DAO類中添加一個方法以公開您的應用程式所需的查詢功能。此方法需要Room運作多個查詢,是以添加 @Transaction注釋以確定整個操作是原子執行的:

@Transaction
@Query("SELECT * FROM User")
public List<UserWithPlaylistsAndSongs> getUsersWithPlaylistsAndSongs();
           

警告:查詢具有嵌套關系的資料需要Room操縱大量資料并可能影響性能。在查詢中使用盡可能少的嵌套關系。

編寫異步DAO查詢

為了防止查詢阻止UI,Room不允許在主線程上通路資料庫。此限制意味着您必須使DAO查詢異步。Room庫包括與多個不同架構的內建,以提供異步查詢執行。

DAO查詢分為三類:

  • One-shot write queries,用于插入,更新或删除資料庫中的資料。
  • One-shot read queries ,僅從資料庫讀取一次資料,然後傳回帶有資料庫快照的結果。
  • Observable read queries 可觀察的讀取查詢,每次基礎資料庫表發生更改時都從資料庫中讀取資料,并發出新值以反映這些更改。

Language and framework options

語言和架構選項

Room提供了與特定語言功能和庫的互操作性的內建支援。下表根據查詢類型和架構顯示适用的傳回類型:

查詢類型 Kotlin語言功能 RxJava RxJava Jetpack Lifecycle
One-shot write Coroutines (suspend)

Single<T>, Maybe<T>, Completable

ListenableFuture<T>

N/A
One-shot read Coroutines (suspend)

Single<T>, Maybe<T>

ListenableFuture<T>

N/A
One-shot read

Flow<T>

Flowable<T>, Publisher<T>, Observable<T>

N/A

LiveData<T>

Kotlin with Flow and couroutines

Kotlin提供的語言功能使您無需第三方架構即可編寫異步查詢:

  • 在2.2或更高版本的Room中,您可以使用Kotlin的 Flow 功能編寫可觀察的查詢。
  • 在2.1或更高版本的Room中,您可以使用suspend關鍵字使用Kotlin協程使您的DAO查詢異步。

注意:要将Kotlin Flow和協程用于Room,必須在build.gradle檔案中包含room-ktx工件。有關更多資訊,請參見 聲明依賴項。

Java with RxJava

如果您的應用程式使用Java程式設計語言,則可以使用RxJava架構中的特殊傳回類型來編寫異步DAO方法。

  • 對于one-shot queries,Room2.1和更高版本支援 Completable, Single<T>以及Maybe<T> 傳回類型

  • 對于observable queries,Room 支援 Publisher<T>, Flowable<T>以及Observable<T> 傳回類型

注意:要将RxJava與Room一起使用,必須在build.gradle檔案中包含room-rxjava2工件。有關更多資訊,請參見聲明依賴項。

Java與LiveData和Guava

如果您的應用程式使用Java程式設計語言,并且您不想使用RxJava架構,則可以使用以下替代方法來編寫異步查詢:

  • 您可以使用Jetpack的LiveData 包裝器類來編寫異步observable queries。
  • 您可以使用Guava的ListenableFuture 包裝器類來編寫異步的one-shot queries

注意:要将Guava與Room一起使用,必須在build.gradle檔案中包含room-guava工件 。有關更多資訊,請參見聲明依賴項。

Write asynchronous one-shot queries

編寫異步一鍵式查詢

一鍵式查詢是僅運作一次并在執行時擷取資料快照的資料庫操作。以下是異步單次查詢的一些示例:

@Dao
interface UserDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertUsers(vararg users: User)

    @Update
    suspend fun updateUsers(vararg users: User)

    @Delete
    suspend fun deleteUsers(vararg users: User)

    @Query("SELECT * FROM user WHERE id = :id")
    suspend fun loadUserById(id: Int): User

    @Query("SELECT * from user WHERE region IN (:regions)")
    suspend fun loadUsersByRegion(regions: List<String>): List<User>
}
           

Write observable queries

編寫可觀察的查詢

可觀察查詢是讀取操作,隻要查詢所引用的任何表發生更改,它們都會發出新值。使用此方法的一種方法是,在插入,更新或删除基礎資料庫中的項目時,幫助您使顯示的項目清單保持最新。以下是一些可觀察查詢的示例:

@Dao
interface UserDao {
    @Query("SELECT * FROM user WHERE id = :id")
    fun loadUserById(id: Int): Flow<User>

    @Query("SELECT * from user WHERE region IN (:regions)")
    fun loadUsersByRegion(regions: List<String>): Flow<List<User>>
}
           

注意:Room中的可觀察查詢有一個重要限制:無論表中的任何行是否更新,無論該行是否在結果集中,該查詢都會重新運作。通過從相應的庫(Flow,RxJava或LiveData)應用distinctUntilChanged()運算符,可以確定隻有在實際查詢結果發生更改時才會通知UI。

Create views into a database

将視圖建立到資料庫中

Room Persistence庫的2.1.0版和更高版本提供對SQLite資料庫視圖的支援,使您可以将查詢封裝到類中。Room将這些查詢支援的類稱為視圖,并且在DAO中使用時,它們的行為與簡單資料對象相同 。

注意:像實體一樣,您可以SELECT針對視圖運作 語句。但是,不能對視圖運作INSERT,UPDATE或DELETE語句。

Create a view

要建立視圖,請将@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;
}
           

Associate a view with your database

将視圖與資料庫關聯

要将此視圖包含在應用程式資料庫中,請将views屬性包含 在應用程式的 @Database注釋中:

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

預先填充您的Room database

有時,您可能希望您的應用程式從已經加載了一組特定資料的資料庫開始。這稱為預填充資料庫。在Room 2.2.0及更高版本中,可以使用API​​方法在初始化時使用裝置檔案系統中預打包的資料庫檔案中的内容預填充Room資料庫。

注意:記憶體中的room資料庫不支援使用createFromAsset()或createFromFile()預填充資料庫。

Prepopulate from an app asset

從 app asset 中預填充

要從位于應用程式資産/目錄中任何位置的預打包資料庫檔案預填充檔案室資料庫,請從RoomDatabase.Builder對象調用build()之前調用createFromAsset() :

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

該createFromAsset()方法接受一個字元串參數,該參數包含從assets/目錄到預打包資料庫檔案的相對路徑。

注意:從asset進行預填充時,Room會驗證資料庫,以確定其架構與預打包資料庫的架構比對。建立預打包的資料庫檔案時,應導出資料庫的架構以用作參考。

Prepopulate from the file system

從檔案系統預填充

要從位于裝置檔案系統中除應用程式assets/目錄之外的任何位置的預打包資料庫檔案預填充Room資料庫,請在調用RoomDatabase.Builder對象中build()方法之前調用createFromFile():

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

該createFromFile()方法接受File預打包資料庫檔案的參數。Room會建立指定檔案的副本,而不是直接打開它,是以請確定您的應用對檔案具有讀取權限。

注意:從檔案系統預填充時,Room會驗證資料庫,以確定其架構與預打包資料庫的架構比對。建立預打包的資料庫檔案時,應導出資料庫的架構以用作參考。

Handle migrations that include prepackaged databases

處理包括預打包資料庫的遷移

預打包的資料庫檔案還可以更改Room資料庫處理回退遷移的方式。通常,當啟用破壞性遷移且Room必須執行遷移而沒有遷移路徑時,Room會删除資料庫中的所有表,并為目标版本建立具有指定架構的空資料庫。但是,如果包含與目标版本号相同的預打包資料庫檔案,Room會在執行破壞性遷移後嘗試使用預打包資料庫檔案的内容填充新建立的資料庫。

有關Room資料庫遷移的更多資訊,請參見遷移Room資料庫。

以下各節提供了一些實際操作示例。

Example: Fallback migration with a prepackaged database

示例:使用預打包的資料庫進行回退遷移

假設以下内容:

  • 您的應用在版本3上定義了Room資料庫。
  • 裝置上已安裝的資料庫執行個體為版本2。
  • 版本3上有一個預打包的資料庫檔案。
  • 從版本2到版本3沒有實作的遷移路徑。
  • 破壞性遷移已啟用。
// Database class definition declaring version 3.
@Database(version = 3)
public abstract class AppDatabase extends RoomDatabase {
    ...
}

// Destructive migrations are enabled and a prepackaged database
// is provided.
Room.databaseBuilder(appContext, AppDatabase.class, "Sample.db")
    .createFromAsset("database/myapp.db")
    .fallbackToDestructiveMigration()
    .build();
           

在這種情況下會發生以下情況:

  1. 由于您的應用程式中定義的資料庫為版本3,而裝置上已安裝的資料庫執行個體為版本2,是以必須進行遷移。
  2. 由于沒有從版本2到版本3的已實施遷移計劃,是以遷移是回退遷移。
  3. 因為fallbackToDestructiveMigration()調用了builder方法,是以

    回退遷移具有破壞性

    。Room

    會删除裝置上已安裝的資料庫執行個體

  4. 由于版本3中有一個預打包的資料庫檔案,是以Room會重新建立資料庫并使用預打包的資料庫檔案的内容填充它。另一方面,如果您預打包的資料庫檔案位于版本2上,則Room會注意到它與目标版本不比對,并且不會将其用作回退遷移的一部分。

Example: Implemented migration with a prepackaged database

示例:使用預打包的資料庫實施遷移

假設您的應用實作了從版本2到版本3的遷移路徑:

// Database class definition declaring version 3.
@Database(version = 3)
public abstract class AppDatabase extends RoomDatabase {
    ...
}

// Migration path definition from version 2 to version 3.
static final Migration MIGRATION_2_3 = new Migration(2, 3) {
    @Override
    public void migrate(SupportSQLiteDatabase database) {
        ...
    }
};

// A prepackaged database is provided.
Room.databaseBuilder(appContext, AppDatabase.class, "Sample.db")
    .createFromAsset("database/myapp.db")
    .addMigrations(MIGRATION_2_3)
    .build();
           

在這種情況下會發生以下情況:

  1. 由于您的應用程式中定義的資料庫為版本3,而裝置上已安裝的資料庫為版本2,是以必須進行遷移。
  2. 由于存在從版本2到版本3的已實作遷移路徑,是以Room運作定義的migrate()方法将裝置上的資料庫執行個體更新到版本3,進而保留資料庫中已經存在的資料。Room不使用預打包的資料庫檔案,因為Room僅在回退遷移的情況下才使用預打包的資料庫檔案。

Example: Multi-step migration with a prepackaged database

示例:使用預打包的資料庫進行多步遷移

預打包的資料庫檔案還可能影響包含多個步驟的遷移。考慮以下情況:

  • 您的應用在版本4上定義了Room資料庫。
  • 裝置上已安裝的資料庫執行個體為版本2。
  • 版本3上有一個預打包的資料庫檔案。
  • 從版本3到版本4有一個已實作的遷移路徑,但沒有從版本2到版本3的遷移路徑。
  • 破壞性遷移已啟用。
// Database class definition declaring version 4.
@Database(version = 4)
public abstract class AppDatabase extends RoomDatabase {
    ...
}

// Migration path definition from version 3 to version 4.
static final Migration MIGRATION_3_4 = new Migration(3, 4) {
    @Override
    public void migrate(SupportSQLiteDatabase database) {
        ...
    }
};

// Destructive migrations are enabled and a prepackaged database is
// provided.
Room.databaseBuilder(appContext, AppDatabase.class, "Sample.db")
    .createFromAsset("database/myapp.db")
    .addMigrations(MIGRATION_3_4)
    .fallbackToDestructiveMigration()
    .build();
           

在這種情況下會發生以下情況:

  1. 由于您的應用程式中定義的資料庫為版本4,而裝置上已安裝的資料庫執行個體為版本2,是以必須進行遷移。
  2. 由于沒有從版本2到版本3的已實作遷移路徑,是以遷移是回退遷移。
  3. 因為fallbackToDestructiveMigration()調用了builder方法,是以回退遷移具有破壞性。Room将裝置上的資料庫執行個體删除。
  4. 由于版本3中有一個預打包的資料庫檔案,是以Room會重新建立資料庫并使用預打包的資料庫檔案的内容填充它。
  5. 裝置上安裝的資料庫現在的版本為3。因為它仍低于應用程式中定義的版本,是以必須進行另一次遷移。
  6. 由于存在從版本3到版本4的已實作遷移路徑,是以Room運作定義的migrate()方法将裝置上的資料庫執行個體更新到版本4,進而保留從版本3預打包的資料庫檔案複制過來的資料。

繼續閱讀