Android architecture component架構(gòu)

簡介

引言

谷歌在今年的I/O大會上發(fā)布了新的架構(gòu)庫Android architecture component雕凹,為了解決開發(fā)者遇到的一些常見問題殴俱,推薦遵從以下兩個原則構(gòu)建應(yīng)用:

  1. 關(guān)注點分離
    盡量避免在Activity或Fragment中編寫所有的代碼,任何不是處理 UI 或操作系統(tǒng)交互的代碼都不應(yīng)該在這些類中枚抵。保持它們盡可能的精簡可以避免許多與生命周期有關(guān)的問題粱挡。
  2. model驅(qū)動UI
    使用持久化的model,因為如果 OS 銷毀應(yīng)用釋放資源俄精,用戶不用擔(dān)心丟失數(shù)據(jù);而且即使網(wǎng)絡(luò)連接不可靠或者是斷開的榕堰,應(yīng)用仍將繼續(xù)運行竖慧。
    為此,推出了新的架構(gòu)組件來幫助開發(fā)者快速搭建滿足上述要求的應(yīng)用逆屡,這就是四個新組件LiveDate,ViewModel,Room Persistence,Lifecycles的由來,而此次的重點就是Room圾旨。

Room

Room提供了一個SQLite之上的抽象層,使得在充分利用SQLite功能的前提下順暢的訪問數(shù)據(jù)庫魏蔗。Room中有三個主要的組件:Entity砍的,Database和Dao。


使用方法

gradle配置

打開整個項目的build.gradle莺治,然后添加:

allprojects {
    repositories {
        jcenter()
        //因為GFW的原因廓鞠,這個網(wǎng)站不一定連的上,參考下文中的問題一
        maven { url 'https://maven.google.com' }
    }
}

然后在app的build.gradle里添加:

// App Toolkit
    compile "android.arch.lifecycle:extensions:1.0.0-alpha1"
    compile "android.arch.persistence.room:runtime:1.0.0-alpha1"
    annotationProcessor "android.arch.lifecycle:compiler:1.0.0-alpha1"
    annotationProcessor "android.arch.persistence.room:compiler:1.0.0-alpha1"

這樣就把所需的庫添加到項目中的谣旁。

Entity

當(dāng)一個類用@Entity注解并且被@Database注解中的entities屬性所引用床佳,Room就會在數(shù)據(jù)庫中為那個entity創(chuàng)建一張表。

默認(rèn)Room會為entity中定義的每一個field都創(chuàng)建一個column榄审。如果一個entity中有你不想持久化的field砌们,那么你可以使用@Ignore來注釋它們,如下面的代碼所示:

@Entity
class User {
    @PrimaryKey
    public int id;

    public String firstName;
    public String lastName;

    @Ignore
    Bitmap picture;
}

要持久化一個field,Room必須有獲取它的渠道浪感。你可以把field寫成public昔头,也可以為它提供一個setter和getter。如果你使用setter和getter的方式影兽,記住它們要基于Room的Java Bean規(guī)范揭斧。

Primary key

每個entity必須至少定義一個field作為主鍵(primary key)。即使只有一個field赢笨,你也必須用@PrimaryKey注釋這個field未蝌。如果你想讓Room為entity設(shè)置自增ID,你可以設(shè)置@PrimaryKey的autoGenerate屬性茧妒。如果你的entity有一個組合主鍵萧吠,你可以使用@Entity注解的primaryKeys屬性,具體用法如下:

@Entity(primaryKeys = {"firstName", "lastName"})
class User {
    public String firstName;
    public String lastName;

    @Ignore
    Bitmap picture;
}

Room默認(rèn)把類名作為數(shù)據(jù)庫的表名桐筏。如果你想用其它的名稱纸型,使用@Entity注解的tableName屬性,如下:

//SQLite中的表名是大小寫敏感的梅忌。
@Entity(tableName = "users")
class User {
    ...
}

和tableName屬性類似狰腌,Room默認(rèn)把field名稱作為數(shù)據(jù)庫表的column名。如果你想讓column有不一樣的名稱牧氮,為field添加@ColumnInfo屬性琼腔,如下:

@Entity(tableName = "users")
class User {
    @PrimaryKey
    public int id;

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

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

    @Ignore
    Bitmap picture;
}

Indices 和 uniqueness

為了提高查詢的效率,你可能想為特定的字段建立索引踱葛。要為一個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;
}

有時候,某個字段或者幾個字段必須是唯一的常挚。你可以通過把@Index注解的unique屬性設(shè)置為true來實現(xiàn)唯一性。下面的代碼防止了一個表中的兩行數(shù)據(jù)出現(xiàn)firstName和lastName字段的值相同的情況:

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

關(guān)系

因為SQLite是關(guān)系數(shù)據(jù)庫洲赵,你可以指定對象之間的關(guān)聯(lián)。雖然大多數(shù)ORM庫允許entity對象相互引用商蕴,但是Room明確禁止了這種行為叠萍。后文中的注意事項中解釋了原因。
雖然不可以使用直接的關(guān)聯(lián)绪商,Room仍然允許你定義entity之間的外鍵(Foreign Key)約束俭令。
比如,假設(shè)有另外一個entity叫做calledBook部宿,你可以使用@ForeignKey注解定義它和User entity之間的關(guān)聯(lián)抄腔,如下:

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

外鍵非常強大瓢湃,因為它允許你指定當(dāng)被關(guān)聯(lián)的entity更新時做什么操作。例如赫蛇,通過在@ForeignKey注解中包含Delete = CASCADE绵患, 你可以告訴SQLite,如果相應(yīng)的User實例被刪除悟耘,那么刪除這個User下的所有book落蝙。

嵌套對象

有時你可能想把一個entity或者一個POJOs作為一個整體看待,即使這個對象包含幾個field暂幼。這種情況下筏勒,你可以使用@Embedded注解,表示你想把一個對象分解為表的子字段旺嬉。然后你就可以像其它獨立字段那樣查詢這些嵌入的字段管行。

比如,我們的User類可以包含一個類型為Address的field邪媳,Address代表street,city,state, 和postCode字段的組合捐顷。為了讓這些組合的字段單獨存放在這個表中,對User類中的Address字段使用@Embedded注解雨效,如下面的代碼所示:

//注:嵌套字段也可以包含其它的嵌套字段迅涮。
class Address {
    public String street;
    public String state;
    public String city;

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

@Entity
class User {
    @PrimaryKey
    public int id;

    public String firstName;

    @Embedded
    public Address address;
}

那么現(xiàn)在代表一個User對象的表就有了如下的字段:id,firstName,street,state,city,以及post_code。
如果一個entity有多個嵌套字段是相同類型徽龟,你可以設(shè)置prefix屬性保持每個字段的唯一性叮姑。Room就會在嵌套對象中的每個字段名的前面添加上這個值。

Data Access Objects (DAOs)

Room中的主要組件是Dao類据悔。DAO抽象出了一種操作數(shù)據(jù)庫的簡便方法传透。

注:Room不允許通過主線程上訪問數(shù)據(jù)庫,除非您在構(gòu)建器上調(diào)用allowMainThreadQueries()屠尊,因為它可能會長時間地鎖定用戶界面。異步查詢(返回LiveData或RxJava Flowable的查詢)將免除此規(guī)則耕拷,因為它們在需要時異步地在后臺線程上運行查詢讼昆。

便利的方法

DAO提供了多種簡便的查詢方式,本文檔列出幾種常見的例子骚烧。

  • insert
    當(dāng)你創(chuàng)建了一個DAO方法并且添加了@Insert注解浸赫,Room生成一個實現(xiàn),將所有的參數(shù)在一次事務(wù)中插入數(shù)據(jù)庫赃绊。
    下面的代碼片段演示了幾種查詢的例子:
@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);
}

如果@Insert方法只接收一個參數(shù)既峡,它可以返回一個long,代表新插入元素的rowId碧查,如果參數(shù)是一個數(shù)組或者集合运敢,那么應(yīng)該返回long[]或者List校仑。

  • update
    Update是一個更新一系列entity的簡便方法。它根據(jù)每個entity的主鍵作為更新的依據(jù)传惠。下面的代碼演示了如何定義這個方法:
@Dao
public interface MyDao {
    @Update
    public void updateUsers(User... users);
}

你可以讓這個方法返回一個int類型的值迄沫,表示更新影響的行數(shù),雖然通常并沒有這個必要卦方。

  • delete
    這個API用于刪除一系列entity羊瘩。它使用主鍵找到要刪除的entity。下面的代碼演示了如何定義這個方法:
@Dao
public interface MyDao {
    @Delete
    public void deleteUsers(User... users);
}

你可以讓這個方法返回一個int類型的值盼砍,表示從數(shù)據(jù)庫中被刪除的行數(shù)尘吗,雖然通常并沒有這個必要。

使用@Query的方法

@Query是DAO類中主要被使用的注解浇坐。它允許你在數(shù)據(jù)庫中執(zhí)行讀寫操作睬捶。每個@Query方法都是在編譯時檢查,因此如果查詢存在問題吗跋,將出現(xiàn)編譯錯誤侧戴,而不是在運行時引起崩潰。
Room還會檢查查詢的返回值跌宛,如果返回的對象的字段名和查詢結(jié)果的相應(yīng)字段名不匹配酗宋,Room將以下面兩種方式提醒你:
如果某些字段名不匹配給出警告。
如果沒有匹配的字段名給出錯誤提示疆拘。
簡單的查詢:

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

這是一個非常簡單的查詢蜕猫,加載所有的user。在編譯時哎迄,Room知道它是查詢user表中的所有字段回右。如果query有語法錯誤,或者user表不存在漱挚,Room將在app編譯時顯示恰當(dāng)?shù)腻e誤信息翔烁。

向query傳遞參數(shù)

大多數(shù)時候,你需要向查詢傳遞參數(shù)來執(zhí)行過濾操作旨涝,比如只顯示大于某個年齡的user蹬屹。為此,在Room注解中使用方法參數(shù)白华,如下:

@Dao
public interface MyDao {
    @Query("SELECT * FROM user WHERE age > :minAge")
    public User[] loadAllUsersOlderThan(int minAge);
}

當(dāng)編譯時處理到這個查詢的時候慨默,Room把:minAge用方法中的minAge匹配。Room使用參數(shù)的名稱來匹配弧腥。如果有不匹配的情況厦取,app編譯的時候就會出現(xiàn)錯誤。
你也可以傳遞多個參數(shù)或者在查詢中多次引用它們管搪,如下面的代碼所示:

@Dao
public interface MyDao {
    @Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge")
    public User[] loadAllUsersBetweenAges(int minAge, int maxAge);

    @Query("SELECT * FROM user WHERE first_name LIKE :search "
           + "OR last_name LIKE :search")
    public List<User> findUserWithName(String search);
}

返回字段的子集

大多數(shù)時候虾攻,我們只需要一個entity的部分字段铡买。比如,你的界面也許只需顯示user的first name 和 last name台谢,而不是用戶的每個詳細(xì)信息寻狂。只獲取UI需要的字段可以節(jié)省可觀的資源,查詢也更快朋沮。
只要結(jié)果的字段可以和返回的對象匹配蛇券,Room允許返回任何的Java對象。比如樊拓,你可以創(chuàng)建如下的POJO獲取user的first name 和 last name:

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

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

現(xiàn)在你可以在query方法中使用這個POJO:

//注:這些POJO也可以使用@Embedded注解纠亚。
@Dao
public interface MyDao {
    @Query("SELECT first_name, last_name FROM user")
    public List<NameTuple> loadFullName();
}

Room知道這個查詢返回first_name和last_name字段的值,并且這些值可以被映射到NameTuple類的field中筋夏。因此Room可以生成正確的代碼蒂胞。如果查詢返回了太多的字段,或者某個字段不在NameTuple類中条篷,Room將顯示一個警告骗随。

傳入?yún)?shù)集合

一些查詢可能需要你傳入個數(shù)是一個變量的參數(shù),只有在運行時才知道具體的參數(shù)個數(shù)赴叹。比如鸿染,你可能想獲取一個區(qū)間的用戶信息。當(dāng)一個參數(shù)代表一個集合的時候Room是知道的乞巧,它在運行時自動根據(jù)提供的參數(shù)個數(shù)擴展涨椒。

@Dao
public interface MyDao {
    @Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
    public List<NameTuple> loadUsersFromRegions(List<String> regions);
}

可觀察的查詢

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

@Dao
public interface MyDao {
    @Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
    public LiveData<List<User>> loadUsersFromRegionsSync(List<String> regions);
}

注:對于version 1.0,Room使用query中的table列表來決定是否更新LiveData對象获三。

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

Direct cursor access

如果你的app需要獲得直接返回的行蚓炬,你可以在查詢中返回Cursor對象松逊,如下面的代碼所示:

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

注:不推薦使用Cursor API,因為它無法保證行是否存在或者行中有哪些值肯夏。只有在當(dāng)前的代碼需要一個cursor经宏,而且你又不好重構(gòu)的時候才使用這個功能犀暑。

多表查詢

某些查詢可能需要根據(jù)多個表查詢出結(jié)果。Room允許你書寫任何查詢烁兰,因此表連接(join)也是可以的耐亏。而且如果響應(yīng)是一個可觀察的數(shù)據(jù)類型,比如Flowable或者LiveData沪斟,Room將觀察查詢中涉及到的所有表广辰。
下面的代碼演示了如何執(zhí)行一個表連接查詢來查出借閱圖書的user與被借出圖書之間的信息。

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

你也可以返回POJO對象主之。比如你可以寫一個如下的查詢加載user與它們的寵物名字:

@Dao
public interface MyDao {
   @Query("SELECT user.name AS userName, pet.name AS petName "
          + "FROM user, pet "
          + "WHERE user.id = pet.user_id")
   public LiveData<List<UserPet>> loadUserAndPetNames();

   // You can also define this class in a separate file, as long as you add the
   // "public" access modifier.
   static class UserPet {
       public String userName;
       public String petName;
   }
}

使用類型轉(zhuǎn)換器

Room內(nèi)置了原始類型择吊。但是,有時你會希望使用自定義數(shù)據(jù)類型槽奕。 要為自定義類型添加這種支持几睛,可以提供一個TypeConverter,它將一個自定義類轉(zhuǎn)換為Room保留的已知類型粤攒。
比如所森,如果我們要保留Date的實例,我們可以編寫以下TypeConverter來存儲數(shù)據(jù)庫中的等效的Unix時間戳記:

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

上述示例定義了兩個函數(shù)夯接,一個將Date對象轉(zhuǎn)換為Long對象焕济,另一個將從Long到Date轉(zhuǎn)換為執(zhí)行逆轉(zhuǎn)換。 由于Room已經(jīng)知道如何持久化Long對象钻蹬,因此可以用此轉(zhuǎn)換器來持久保存Date類型的值吼蚁。
接下來,將@TypeConverters注釋添加到AppDatabase類问欠,以便Room可以使用你為該AppDatabase中的每個實體和DAO定義的轉(zhuǎn)換器:

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

使用這些轉(zhuǎn)換器肝匆,可以在其他查詢中使用自定義類型,就像使用原始類型一樣顺献,如以下代碼片段所示:

//User.java
@Entity
public class User {
    ...
    private Date birthday;
}
//UserDao.java
@Dao
public interface UserDao {
    ...
    @Query("SELECT * FROM user WHERE birthday BETWEEN :from AND :to")
    List<User> findUsersBornBetweenDates(Date from, Date to);
}

測試遷移

寫遷移不是一件簡單的事情旗国,如果寫法不恰當(dāng)可能導(dǎo)致app的進(jìn)入崩潰的惡性循環(huán)。為了保證app的穩(wěn)定性注整,你應(yīng)該先測試遷移能曾。Room提供了一個testing Maven artifact來幫助你完成這個測試過程。但是要讓這個artifact工作肿轨,需要導(dǎo)出數(shù)據(jù)庫的schema寿冕。

導(dǎo)出schemas

在編譯的時候,Room將database的schema信息導(dǎo)出到一個JSON文件中椒袍。為此驼唱,要在build.gradle 文件中設(shè)置room.schemaLocation注解處理器屬性,如下面的代碼所示:
build.gradle

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

你應(yīng)該把導(dǎo)出的在這個JSON文件-它代表了你的數(shù)據(jù)庫的schema歷史-保存到你的版本管理系統(tǒng)中驹暑,這樣就可以讓Room創(chuàng)建舊版本的數(shù)據(jù)庫來測試玫恳。

測試遷移需要把Room的Maven artifact android.arch.persistence.room:testing 添加到你的test dependencies,并且把schema的位置作為 asset folder添加進(jìn)去辨赐,代碼如下:
build.gradle

android {
    ...
    sourceSets {
        androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
    }
}

testing package提供了一個MigrationTestHelper類,它可以讀出這些schema文件京办。它同時也是一個 Junit4 TestRule類掀序,因此可以管理創(chuàng)建的數(shù)據(jù)庫。

下面的代碼是一個遷移測試的例子:

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

    @Rule
    public MigrationTestHelper helper;

    public MigrationTest() {
        helper = new MigrationTestHelper(InstrumentationRegistry.getContext(),
                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.
    }
}

可能出現(xiàn)的問題

問題1

問題描述

編譯項目消耗時間長惭婿,而且報錯

compile "android.arch.lifecycle:runtime:1.0.0-alpha1"
compile "android.arch.lifecycle:extensions:1.0.0-alpha1"
annotationProcessor "android.arch.lifecycle:compiler:1.0.0-alpha1"

解決辦法

純粹是因為greatWall的問題不恭,把app的build.gradle中的maven.google.com替換https://dl.google.com/dl/android/maven2/

問題2

問題描述

在測試數(shù)據(jù)庫遷移的時候,需要往build.gradle上添加一句

    testCompile "android.arch.persistence.room:testing:1.0.0-alpha5"

然后編譯的時候可能會報錯:


這里寫圖片描述
這里寫圖片描述

解決辦法

把依賴修改為

    compile "android.arch.persistence.room:testing:1.0.0-alpha5"

注意事項

事項1

兩個entity之間不能添加對象引用
官網(wǎng)翻譯:在數(shù)據(jù)庫和相關(guān)的對象模型之間建立映射關(guān)系是服務(wù)端非常常見且好用的辦法财饥,可以通過懶加載來提高性能县袱。
然而,在客戶端這邊佑力,懶加載的效果并不顯著因為它發(fā)生在UI線程式散,而在UI線程里面執(zhí)行對磁盤的查找會帶來顯著的性能問題。UI線程大概會花16ms的時間來計算與繪制頁面布局的升級打颤,所以就算查找只花5ms暴拄,app仍然可能在繪制框架的過程中耗盡時間并且造成可見的問題。更糟糕的是编饺,如果查找是多個事務(wù)并行執(zhí)行的話可能會花掉更多時間乖篷,或者設(shè)備同時在執(zhí)行其他磁盤耗費嚴(yán)重的任務(wù)。而且如果不使用懶加載的話透且,將會帶來更多內(nèi)存上的消耗撕蔼。
ORM通常將這個問題留給開發(fā)者自己根據(jù)應(yīng)用的實際情況做決定。不幸的是秽誊,開發(fā)者通常會在模型層與UI之間共享模型鲸沮。隨著UI不停變更,很難去分析與調(diào)試出現(xiàn)的問題锅论。

數(shù)據(jù)統(tǒng)計

操作 greendao耗時 room耗時
循環(huán)插入10000條數(shù)據(jù) 98238ms 66821ms
批量插入10000條數(shù)據(jù) 623ms 1051ms
循環(huán)更新10000次 97840ms 61348ms
批量更新10000條數(shù)據(jù) 739ms 1366ms
循環(huán)查詢10000條數(shù)據(jù) 31923ms 6932ms
批量查詢10000條數(shù)據(jù) 149ms 不支持大量參數(shù)的同時查詢

greenDao 的數(shù)據(jù)來源于網(wǎng)上讼溺。

操作 greendao耗時 room耗時
循環(huán)插入10000條數(shù)據(jù) 98238ms 66821ms
批量插入10000條數(shù)據(jù) 623ms 1051ms
循環(huán)更新10000次 97840ms 61348ms
批量更新10000條數(shù)據(jù) 739ms 1366ms
循環(huán)查詢10000條數(shù)據(jù) 31923ms 6932ms
批量查詢10000條數(shù)據(jù) 149ms 不支持大量參數(shù)的同時查詢
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市最易,隨后出現(xiàn)的幾起案子怒坯,更是在濱河造成了極大的恐慌,老刑警劉巖藻懒,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件剔猿,死亡現(xiàn)場離奇詭異,居然都是意外死亡嬉荆,警方通過查閱死者的電腦和手機归敬,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人弄慰,你說我怎么就攤上這事〉妫” “怎么了陆爽?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長扳缕。 經(jīng)常有香客問我慌闭,道長,這世上最難降的妖魔是什么躯舔? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任驴剔,我火速辦了婚禮,結(jié)果婚禮上粥庄,老公的妹妹穿的比我還像新娘丧失。我一直安慰自己,他們只是感情好惜互,可當(dāng)我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布布讹。 她就那樣靜靜地躺著,像睡著了一般训堆。 火紅的嫁衣襯著肌膚如雪描验。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天坑鱼,我揣著相機與錄音膘流,去河邊找鬼。 笑死鲁沥,一個胖子當(dāng)著我的面吹牛呼股,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播画恰,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼卖怜,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了阐枣?” 一聲冷哼從身側(cè)響起马靠,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎蔼两,沒想到半個月后甩鳄,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡额划,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年妙啃,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡揖赴,死狀恐怖馆匿,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情燥滑,我是刑警寧澤渐北,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站铭拧,受9級特大地震影響赃蛛,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜搀菩,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一呕臂、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧肪跋,春花似錦歧蒋、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至易桃,卻和暖如春褥琐,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背晤郑。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工敌呈, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人造寝。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓磕洪,卻偏偏與公主長得像,于是被迫代替她去往敵國和親诫龙。 傳聞我的和親對象是個殘疾皇子析显,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內(nèi)容