Room框架學(xué)習(xí)、使用

Room Persistence Library(官方介紹)
官方ORM(Object Relational Mapping)框架專題
Google官方推出的Android架構(gòu)組件系列文章(六)Room持久化庫

Room 的官方API 可以查看這里

Room


介紹

Room是谷歌官方的數(shù)據(jù)庫ORM(對象關(guān)系映射)框架,使用起來非常方便。

Room提供了一個SQLite之上的抽象層,使得在充分利用SQLite功能的前提下順暢的訪問數(shù)據(jù)庫。

對于需要處理大量結(jié)構(gòu)化數(shù)據(jù)的App來說,把這些數(shù)據(jù)做本地持久化會帶來很大的好處。常見的用例是緩存重要數(shù)據(jù)塊。這樣當(dāng)設(shè)備無法連網(wǎng)的時候,用戶仍然可以瀏覽內(nèi)容。而用戶對內(nèi)容做出的任何改動都在網(wǎng)絡(luò)恢復(fù)的時候同步到服務(wù)端。


引入

1、項目build.gradle中添加如下代碼倉庫

allprojects {
    repositories {
        jcenter()
        google()
    }
}

2、app Module中引入

 // Room依賴
    implementation 'android.arch.persistence.room:runtime:1.1.0'
    annotationProcessor "android.arch.persistence.room:compiler:1.1.0"

組成

Room中有三個主要的組件:

1、Database

數(shù)據(jù)庫組件,底層連接的主要入口,主要作用:

  • 創(chuàng)建database holder
  • 使用注解定義實(shí)體類
  • 實(shí)體類定義了從數(shù)據(jù)庫中獲取數(shù)據(jù)的對象(DAO)

這個被注解的類是一個繼承RoomDatabase的抽象類。在運(yùn)行時,可以通過調(diào)用Room.databaseBuilder() 或者 Room.inMemoryDatabaseBuilder()來得到它的實(shí)例。


2、Entity

實(shí)體類組件, 一個類表示數(shù)據(jù)庫的一個表。

注意

  • 1、你必須在Database類中的entities數(shù)組中引用這些entity類
  • 2、entity中的每一個field都將被持久化到數(shù)據(jù)庫,除非使用了@Ignore注解。

3、DAO

DAO查詢組件,DAO(Data Access Object) 數(shù)據(jù)訪問對象是一個面向?qū)ο蟮臄?shù)據(jù)庫接口。
DAO是Room的主要組件,負(fù)責(zé)定義查詢(添加或者刪除等)數(shù)據(jù)庫的方法。


4、示意圖

image

示例代碼

1、User.java ---- 實(shí)體類組件(Entity)

  • 建表:當(dāng)一個類用@Entity注解并且被@Database注解中的entities屬性所引用時(@Database(entities = {User.class}, version = 1)),Room就會在數(shù)據(jù)庫中為那個entity創(chuàng)建一張表。
  • 表名:Room默認(rèn)把類名作為數(shù)據(jù)庫的表名,自定義表名需要使用@Entity注解的tableName屬性,@Entity(tableName = "users")。
  • 建列:默認(rèn)Room會為實(shí)體類中定義的每一個字段(field)都創(chuàng)建一個數(shù)據(jù)表列(column)。
  • 列名:默認(rèn)使用字段名作為列名,如果想指定列名,可以使用 @ColumnInfo(name = "your_name")。
  • 持久化:要持久化一個字段(字段數(shù)據(jù)寫入數(shù)據(jù)庫),Room必須有獲取它的渠道。你可以把字段寫成public,也可以為它提供一個setter和getter。如果你使用setter和getter的方式,記住它們要基于Room的Java Bean規(guī)范。
  • 忽略:如果一個實(shí)體類中有你不想持久化的字段,那么你可以使用@Ignore來注釋它們。
  • 主鍵:每個實(shí)體類Entity都必須至少定義一個field作為主鍵(primary key),主鍵自增需要使用@PrimaryKey的autoGenerate屬性。
  • 組合主鍵:需要使用@Entity注解的primaryKeys屬性,比如@Entity(primaryKeys = {"firstName", "lastName"}),多個字段聯(lián)合形成一個主鍵組合,保證主鍵的唯一性
@Entity(tableName = "users")
public class User {
    @PrimaryKey(autoGenerate = true)
    private int uid;
 
    @ColumnInfo(name = "first_name")
    private String firstName;
 
    @ColumnInfo(name = "last_name")
    private String lastName;
 
    @Ignore
    Bitmap picture;
}

2、UserDao.java ---- DAO查詢組件

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

3、AppDatabase.java ---- 數(shù)據(jù)庫組件

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

4、獲取database實(shí)例

獲取database實(shí)例的時候應(yīng)該保持單例模式,因?yàn)閿?shù)據(jù)庫的實(shí)例對內(nèi)存的開銷是比較大的,而且程序內(nèi)一般也不需要多個database的實(shí)例。

AppDatabase db = Room.databaseBuilder(getApplicationContext(),AppDatabase.class, "database-name").build();

相關(guān)概念

1、索引(Indices )

為了提高查詢的效率,可能給特定的字段建立索引。
要為一個entity添加索引,在@Entity注解中添加indices屬性,列出你想放在索引或者組合索引中的字段。
代碼示例:

@Entity(indices = {@Index("name"), @Index("last_name", "address")})
class User {
    @PrimaryKey
    public int id;
 
    public String firstName;
    public String address;
 
    @ColumnInfo(name = "last_name")
    public String lastName;
 
    @Ignore
    Bitmap picture;
}

2、唯一性(uniqueness)

指定某個字段或者幾個字段的值必須是唯一的,比如用戶名或手機(jī)號之類的賬戶唯一標(biāo)識字段。
可以通過把@Index注解的unique屬性設(shè)置為true來實(shí)現(xiàn)唯一性

@Entity(indices = {@Index(value = {"first_name", "last_name"},
        unique = true)})
class User {
    @PrimaryKey
    public int id;
 
    @ColumnInfo(name = "first_name")
    public String firstName;
 
    @ColumnInfo(name = "last_name")
    public String lastName;
 
    @Ignore
    Bitmap picture;
}

3、外鍵約束(Foreign Key)

一個表中的外鍵(Foreign Key) 指向另一個表中的主鍵(Primary Key),在更新和刪除時起到約束的作用。比如,如果你想在刪除主鍵表中的一條數(shù)據(jù)時可以同時刪除外鍵約束表中相對應(yīng)的數(shù)據(jù),你可以在@ForeignKey注解中加上onDelete=CASCADE。

下面的代碼就指定了Book表中的user_id字段為User表的外鍵,與User表的id字段一一對應(yīng),使用Entity的foreignKeys屬性指定,寫法如下:

@Entity(foreignKeys = @ForeignKey(entity = User.class,
                                  parentColumns = "id",
                                  childColumns = "user_id"))
class Book {
    @PrimaryKey
    public int bookId;
 
    public String title;
 
    @ColumnInfo(name = "user_id")
    public int userId;
}
  • 注意
    替換沖突『 @Insert(onConfilict=REPLACE) 』不適用于外鍵約束,onConfilict不是單獨(dú)的sql命令,可以理解為一組REMOVE和REPLACE的操作,請參見SQLite文檔的ON_CONFLICT語句,onConfilict有如下五種沖突解決算法。

4、對象嵌套

就是一個實(shí)體類中嵌入另一個實(shí)體類,可以多層嵌套。比如你在User中嵌套Address,如果你使用@Embedded注解Address的話,那么User表中就擁有了Address的所有字段了。
為了防止多個實(shí)體嵌套造成字段重復(fù),你可以通過設(shè)置prefix屬性來保持每列的唯一性。Room會將提供的值添加到嵌入對象的每個列名的開頭。

@Entity
class User {
    @PrimaryKey
    public int id;
 
    public String firstName;
 
    @Embedded
    public Address address;
}
class Address {
    public String street;
    public String state;
    public String city;
 
    @ColumnInfo(name = "post_code")
    public int postCode;
}

數(shù)據(jù)查詢(DAO)

DAO抽象出了一種操作數(shù)據(jù)庫的簡便方法。下面介紹一下常見的查詢方式。

1、新增、插入(Insert)

創(chuàng)建一個DAO方法并使用@Insert注解,Room就會在工作線程中將所有參數(shù)插入到數(shù)據(jù)庫。
如果@Insert方法僅僅接收一個參數(shù),那它可以返回一個long,表示插入項的rowId。如果參數(shù)是一個數(shù)組或集合,它會返回long []或List<Long>。

@Dao
public interface MyDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    public void insertUsers(User... users);
 
    @Insert
    public void insertBothUsers(User user1, User user2);
 
    @Insert
    public void insertUsersAndFriends(User user, List<User> friends);
}

2、修改、更新(Update)

根據(jù)每個entity的主鍵作為更新的依據(jù),此方法可以返回一個int值,指示數(shù)據(jù)庫中更新的行數(shù)。

@Dao
public interface MyDao {
    @Update
    public void updateUsers(User... users);
}

3、刪除(Delete)

使用主鍵找到要刪除的entity,此方法可以返回一個int值,指示數(shù)據(jù)庫中被刪除的行數(shù)。

@Dao
public interface MyDao {
    @Delete
    public void deleteUsers(User... users);
}

4、查詢(Query)

@Query(查詢)是DAO類中使用的主要注解。可以讓你執(zhí)行數(shù)據(jù)庫讀/寫操作。每個@Query方法都會在編譯時驗(yàn)證,如果查詢語句有問題,會發(fā)生編譯錯誤而不是運(yùn)行時故障。

Room還會檢查查詢的返回值,如果返回的對象字段名和查詢結(jié)果的相應(yīng)字段名不匹配,Room將以下面兩種方式提醒你:

  • 如果是某些字段名不匹配會給出警告。
  • 如果沒有匹配的字段名則會給出錯誤提示。

4-1、簡單查詢

@Dao
public interface MyDao {
    @Query("SELECT * FROM user")
    public User[] loadAllUsers();
}

4-2、條件查詢 (傳參)

注意查詢方法的入?yún)⒃诓樵冋Z句中的寫法“:minAge”,另外,在使用in操作符進(jìn)行查詢時,別忘記加上“()”哦!

@Dao
public interface MyDao {
    //查詢User表中年齡大于入?yún)⒌臄?shù)據(jù)集合
    @Query("SELECT * FROM user WHERE age > :minAge")
    public User[] loadAllUsersOlderThan(int minAge);

    //查詢User表中年齡介于入?yún)⒅g的數(shù)據(jù)集合
    @Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge")
    public User[] loadAllUsersBetweenAges(int minAge, int maxAge);
 
   //查詢User表中名字中入?yún)㈥P(guān)鍵字的數(shù)據(jù)集合
    @Query("SELECT * FROM user WHERE first_name LIKE :search "
           + "OR last_name LIKE :search")
    public List<User> findUserWithName(String search);

    //查詢User表中id符合入?yún)⒓现械臄?shù)據(jù)集合
    @Query("SELECT * FROM user WHERE uid IN (:userIds)")
    List<User> loadAllByIds(int[] userIds);

}

4-3、部分查詢

有時候我們僅僅需要的是數(shù)據(jù)表中的部分?jǐn)?shù)據(jù),這個時候我們可以指定DAO的查詢方法只返回我們需要的字段,這樣不僅節(jié)約資源而且查詢更快。

方法是在查詢語句中指定需要獲取的字段,然后指定對應(yīng)的實(shí)體類來獲取查詢的返回數(shù)據(jù)。

例如,User的實(shí)際字段有如下四個,但是我們只需要其中的first_name和last_name,那么我們可以重新定義一個實(shí)體類UserName,然后在查詢方法中指定只查詢first_name和last_name字段,并使用UserName實(shí)體來獲取查詢語句的返回數(shù)據(jù)。

注:這些“裁剪”的實(shí)體類也是可以使用@Embedded注解的。

@Entity(tableName = "users")
public class User {
    @PrimaryKey(autoGenerate = true)
    private int uid;

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

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

    @Ignore
    Bitmap picture;
}
public class UserName{
    @ColumnInfo(name="first_name")
    public String firstName;
 
    @ColumnInfo(name="last_name")
    public String lastName;
}
@Dao
public interface MyDao {
    @Query("SELECT first_name, last_name FROM user")
    public List<NameTuple> loadFullName();
}

4-4、原始查詢 RawQuery

我們可以利用 RawQuery 進(jìn)行原始SQL語句查詢,示例代碼:

 @Dao
 interface RawDao {
     @RawQuery
     User getUserViaQuery(SupportSQLiteQuery query);
 }
 SimpleSQLiteQuery query = new SimpleSQLiteQuery("SELECT * FROM User WHERE id = ? LIMIT 1", new Object[]{userId});
 User user2 = rawDao.getUserViaQuery(query);

1、Room將根據(jù)函數(shù)的返回類型("User ")生成代碼,如果未能通過正確的查詢,將導(dǎo)致運(yùn)行時異常。
2、RawQuery方法只能用于讀取查詢。對于寫入查詢,請使用RoomDatabase.getOpenHelper().getWritableDatabase()


5、可觀察的查詢(Observable queries)

5-1、Query

當(dāng)執(zhí)行查詢的時候,你通常希望app的UI能自動在數(shù)據(jù)更新的時候更新。為此,在query方法中使用 LiveData 類型的返回值。當(dāng)數(shù)據(jù)庫變化的時候,Room會生成所有的必要代碼來更新LiveData。

@Dao
public interface MyDao {
    @Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge")
     public LiveData<List<User>> loadAllUsersBetweenAges(int minAge, int maxAge);
}

使用步驟:

  • 創(chuàng)建一個LiveData實(shí)例來保存某種類型的數(shù)據(jù)。 這通常在您的ViewModel類中完成。
public class UserViewModel extends ViewModel {

private LiveData<List<User>> mUsers;

public LiveData<List<User>> getUsers() {
    if (mUsers== null) {
        mUsers= MyDao.loadAllUsersBetweenAges(10,30);
    }
    return mUsers;
}

...
}
  • 創(chuàng)建一個Observer對象,該對象定義onChanged()方法,該方法在LiveData對象的數(shù)據(jù)發(fā)生變化時回調(diào), 通常是在UI控制器中創(chuàng)建Observer對象,比如Activity或Fragment。
public class UserActivity extends AppCompatActivity {

    private UserViewModel mModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mModel = ViewModelProviders.of(this).get(UserViewModel.class);
 
        final Observer<List<User>> userObserver = new Observer<List<User>>() {
            @Override
            public void onChanged(@Nullable List<User> users) {
                //update the ui
            }
        };

        mModel.getCurrentName().observe(this, userObserver);
    }
}
  • 使用observe()方法將Observer對象附加到LiveData對象。 observe()方法使用LifecycleOwner對象。 這將Observer對象訂閱到LiveData對象,以便通知其更改。 您通常將Observer對象附加到UI控制器中,比如Activity或Fragment。

5-2、RawQuery

RawQuery方法可以返回可觀察的類型,但您需要使用注釋中的observedEntities()字段指定在查詢中訪問哪些表。
代碼示例:

 @Dao
 interface RawDao {
     @RawQuery(observedEntities = User.class)
     LiveData<List<User>> getUsers(SupportSQLiteQuery query);
 }
 LiveData<List<User>> liveUsers = rawDao.getUsers(
     new SimpleSQLiteQuery("SELECT * FROM User ORDER BY name DESC"));

Rxjava

Room還可以讓你定義的查詢返回RxJava2的Publisher和Flowable對象。要使用這個功能,在Gradle dependencies中添加android.arch.persistence.room:rxjava2。然后你就可以返回RxJava2中定義的對象類型了,如下面的代碼所示:

@Dao
public interface MyDao {
    @Query("SELECT * from user where id = :id LIMIT 1")
    public Flowable<User> loadUserById(int id);
}

Cursor

如果你的app需要直接獲得返回的行,你可以在查詢中返回Cursor對象。但是非常不鼓勵使用Cursor ,因?yàn)樗鼰o法保證行是否存在,或者行包含什么值。

public interface MyDao {
    @Query("SELECT * FROM user WHERE age > :minAge LIMIT 5")
    public Cursor loadRawUsersOlderThan(int minAge);
}

多表查詢

一些查詢可能要求查詢多張表來計算結(jié)果。Room允許你書寫任何查詢,因此表連接(join)也是可以的。
而且如果響應(yīng)是一個可觀察的數(shù)據(jù)類型,比如Flowable或者LiveData,Room將觀察查詢中涉及到的所有表,檢測出所有的無效表。

下面的代碼演示了如何執(zhí)行一個表連接查詢來查出借閱圖書的user與被借出圖書之間的信息。
邏輯:入?yún)serName ---- (user.name LIKE :userName) ----> user ---- (user.id = loab.userid) ----> loab ---- (loan.book_id = book.id) ----> book
語句:book <---- (loan.book_id = book.id) ---- loab <---- (user.id = loab.userid) ---- user <---- (user.name LIKE :userName) ---- userName

@Dao
public interface MyDao {
   @Query("SELECT * FROM book "
          + "INNER JOIN loan ON loan.book_id = book.id "
          + "INNER JOIN user ON user.id = loan.user_id "
          + "WHERE user.name LIKE :userName")
  public List<Book> findBooksBorrowedByNameSync(String userName);
}
  • INNER JOIN:內(nèi)連接,顯示左表和右表符合連接條件的記錄
  • JOIN: 如果表中有至少一個匹配,則返回行
  • LEFT JOIN: 即使右表中沒有匹配,也從左表返回所有的行
  • RIGHT JOIN: 即使左表中沒有匹配,也從右表返回所有的行
  • FULL JOIN: 只要其中一個表中存在匹配,就返回行

類型轉(zhuǎn)換

1、Room對Java的基本數(shù)據(jù)類型以及其包裝類型都提供了支持
2、但是有時候你可能使用了一個自定義的數(shù)據(jù)類型,并且你想將此類型的數(shù)據(jù)存儲到數(shù)據(jù)庫表中的字段里。

為了實(shí)現(xiàn)自定義數(shù)據(jù)類型的轉(zhuǎn)換,你需要一個類型轉(zhuǎn)換器TypeConverter,它將負(fù)責(zé)處理自定義數(shù)據(jù)類和Room可以保存的已知類型之間的轉(zhuǎn)換。

比如,如果我們想要保存Date實(shí)例,那么第一步

  • 寫一個TypeConverter類,實(shí)現(xiàn)Date類型和Long類型的數(shù)據(jù)轉(zhuǎn)換
public class Converters {
    @TypeConverter
    public static Date fromTimestamp(Long value) {
        return value == null ? null : new Date(value);
    }

    @TypeConverter
    public static Long dateToTimestamp(Date date) {
        return date == null ? null : date.getTime();
    }
}

可以看到上面的轉(zhuǎn)換類,提供了兩個方法以實(shí)現(xiàn)Date類型和Long類型的數(shù)據(jù)相互轉(zhuǎn)換。

  • 使用TypeConverter類,實(shí)現(xiàn)持久化Date類型的數(shù)據(jù)
    這里就需要把這個轉(zhuǎn)換類Converters添加到我們的數(shù)據(jù)庫組件AppDatabase中了
@Database(entities = {User.java}, version = 1)
@TypeConverters({Converter.class})
public abstract class AppDatabase extends RoomDatabase {
    public abstract UserDao userDao();
}

數(shù)據(jù)庫升級

  • 基本使用
    實(shí)現(xiàn)就是在對應(yīng)的Database中通過Migration進(jìn)行升級,使用的方式是:
    1、利用Migration 構(gòu)建數(shù)據(jù)庫升級語句
    //數(shù)據(jù)庫升級
    private static final Migration migration_1_2 = new Migration(1, 2) {
        @Override
        public void migrate(@NonNull SupportSQLiteDatabase database) {
            
        }
    };

2、在數(shù)據(jù)庫的構(gòu)造方法中通過addMigrations()方法傳入對應(yīng)的Migration

 public synchronized static AppDatabase getInstance(byte[] passphrase) {
        if (INSTANCE == null) {
            synchronized (AppDatabase.class) {
                if (INSTANCE == null) {
                    INSTANCE = Room
                            .databaseBuilder(mContext.getApplicationContext(), AppDatabase.class, sDbPath)
                            .openHelperFactory(new HelperFactory(passphrase))
                            .allowMainThreadQueries()
                            .addMigrations(migration_1_2)
                            .build();
                }

            }
        }
        return INSTANCE;
    }

3、數(shù)據(jù)庫版本(version )+1

@Database(entities = {MarkBook.class}, version = 2, exportSchema = false)
public abstract class AppDatabase extends RoomDatabase {
....

總結(jié):就是把數(shù)據(jù)庫的變化通過SQL語句傳到數(shù)據(jù)庫的構(gòu)造方法中。


  • 常用的幾種數(shù)據(jù)庫升級

新增表

    //數(shù)據(jù)庫升級
    private static final Migration migration_1_2 = new Migration(1, 2) {
        @Override
        public void migrate(@NonNull SupportSQLiteDatabase database) {
            database.execSQL("CREATE TABLE IF NOT EXISTS SYMBOL_SETTING (_id INTEGER primary key NOT NULL, dbName TEXT, symbolJson TEXT)");
        }
    };

增加字段

    //數(shù)據(jù)庫升級
    private static final Migration migration_1_2 = new Migration(1, 2) {
        @Override
        public void migrate(@NonNull SupportSQLiteDatabase database) {
            database.execSQL("ALTER TABLE SURVEY_RECORD ADD STATUS INTEGER  DEFAULT 0");
        }
    };

數(shù)據(jù)庫多次升級

    //數(shù)據(jù)庫升級,SURVEY_RECORD新增SJBZ、SZFHZZZB字段
    private static final Migration migration_2_3 = new Migration(2, 3) {
        @Override
        public void migrate(@NonNull SupportSQLiteDatabase database) {
            database.execSQL("ALTER TABLE SURVEY_RECORD ADD SJBZ TEXT");
            database.execSQL("ALTER TABLE SURVEY_RECORD ADD SZFHZZZB TEXT");
        }
    };

    //數(shù)據(jù)庫升級
    private static final Migration migration_1_2 = new Migration(1, 2) {
        @Override
        public void migrate(@NonNull SupportSQLiteDatabase database) {
            database.execSQL("ALTER TABLE SURVEY_RECORD ADD STATUS INTEGER  DEFAULT 0");
        }
    };

    public synchronized static TaskDatabase getInstance(byte[] passphrase) {
        if (INSTANCE == null) {
            synchronized (TaskDatabase.class) {
                if (INSTANCE == null) {
                    INSTANCE = Room
                            .databaseBuilder(mContext.getApplicationContext(), TaskDatabase.class, sDbPath)
                            .openHelperFactory(new HelperFactory(passphrase))
                            .allowMainThreadQueries()
                            .addMigrations(migration_1_2,migration_2_3)
                            .build();
                }

            }
        }
        return (INSTANCE);
    }

備注:數(shù)據(jù)庫升級的時候最好文字說明一下這次升級了什么,不要因?yàn)橛蠸QL語句就不寫注解了,這樣不好。

以普通的方式打開SQLite數(shù)據(jù)庫

有時候我們需要以普通的方式打開SQLite數(shù)據(jù)庫,以方便我們使用原生的更新、刪除、查詢語句

TaskDatabase database = TaskDatabase.getInstance(Encryption.getInstance().getDBEncryptionPassword());
SupportSQLiteDatabase db = database.getOpenHelper().getWritableDatabase();
db.execSQL("");
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容