Android官方數(shù)據(jù)庫(kù)框架-Room

Room.jpeg

前言

Android 開(kāi)發(fā)者使用數(shù)據(jù)庫(kù)的時(shí)候,最先想到的是 SQLite呢撞。如果有對(duì)外公開(kāi)的需求损姜,則需再包裝一層 ContentProvider。除此之外殊霞,也可以選擇開(kāi)源的數(shù)據(jù)庫(kù)框架摧阅,比如 GreenDaoDBFlow等绷蹲。

本文將講述 Google 推出的數(shù)據(jù)庫(kù)框架 Room棒卷,和您一起探討: 如何使用 Room、其實(shí)現(xiàn)的大致原理以及它的優(yōu)勢(shì)祝钢。

簡(jiǎn)介

Room 是房間的意思比规。房間除了能存放物品,還能帶給人溫暖和安心的感覺(jué)拦英。用 Room 給這個(gè)抽象的軟件架構(gòu)命名蜒什,增加了人文色彩,很有溫度疤估。

先來(lái)看一下 Room 框架的基本組件灾常。

Room框架的基本組件.png

使用起來(lái)大體就是這幾個(gè)步驟,很便捷做裙。

Room使用步驟.png

使用前需要構(gòu)筑如下依賴岗憋。

dependencies {
  def room_version = "2.2.6"

  implementation "androidx.room:room-runtime:$room_version"
  kapt "androidx.room:room-compiler:$room_version"

  implementation "androidx.room:room-ktx:$room_version"
  testImplementation "androidx.room:room-testing:$room_version"
}

實(shí)戰(zhàn)

下面將通過(guò)一個(gè)展示電影列表的 demo 演示 Room 框架的使用肃晚。

組件構(gòu)建

首先構(gòu)建一個(gè)電影表 Movie锚贱,有名稱、演員关串、上映年份拧廊、評(píng)分這么幾個(gè)字段监徘。

@Entity
class Movie() : BaseObservable() {
    @PrimaryKey(autoGenerate = true)
    var id = 0

    @ColumnInfo(name = "movie_name", defaultValue = "Harry Potter")
    lateinit var name: String

    @ColumnInfo(name = "actor_name", defaultValue = "Jack Daniel")
    lateinit var actor: String

    @ColumnInfo(name = "post_year", defaultValue = "1999")
    var year = 1999

    @ColumnInfo(name = "review_score", defaultValue = "8.0")
    var score = 8.0
}
  • @Entity 表示數(shù)據(jù)庫(kù)中的表

  • @PrimaryKey 表示主鍵,autoGenerate 表示自增

  • @ColumnInfo 表示字段吧碾,name 表示字段名稱

然后構(gòu)建一個(gè)訪問(wèn) Movie 表的 DAO 接口凰盔。

interface MovieDao {
    @Insert
    fun insert(vararg movies: Movie?): LongArray?

    @Delete
    fun delete(movie: Movie?): Int

    @Update
    fun update(vararg movies: Movie?): Int

    @get:Query("SELECT * FROM movie")
    val allMovies: LiveData<List<Movie?>?>
}
  • @Dao 表示訪問(wèn) DB 的方法,需要聲明為接口或抽象類倦春,編譯階段將生成 _Impl 實(shí)現(xiàn)類户敬,此處則將生成 MovieDao_Impl.java 文件

  • @Insert@Delete睁本、@Update@Query 分別表示數(shù)據(jù)庫(kù)的增刪改查方法

最后需要構(gòu)建 Room 使用的入口 RoomDatabase尿庐。

@Database(entities = [Movie::class], version = 1)
abstract class MovieDataBase : RoomDatabase() {
    abstract fun movieDao(): MovieDao

    companion object {
        @Volatile
        private var sInstance: MovieDataBase? = null
        private const val DATA_BASE_NAME = "jetpack_movie.db"

        @JvmStatic
        fun getInstance(context: Context): MovieDataBase? {
            if (sInstance == null) {
                synchronized(MovieDataBase::class.java) {
                    if (sInstance == null) {
                        sInstance = createInstance(context)
                    }
                }
            }
            return sInstance
        }

        private fun createInstance(context: Context): MovieDataBase {
            return Room.databaseBuilder(context.applicationContext, MovieDataBase::class.java, DATA_BASE_NAME)
                    ...
                    .build()
        }
    }
}
  • @Database 表示繼承自 RoomDatabase 的抽象類,entities 指定表的實(shí)現(xiàn)類列表呢堰,version 指定了 DB 版本

  • 必須提供獲取 DAO 接口的抽象方法,比如上面定義的 movieDao()枉疼,Room 將通過(guò)這個(gè)方法實(shí)例化 DAO 接口

  • RoomDatabase 實(shí)例的內(nèi)存開(kāi)銷較大皮假,建議使用單例模式管理

  • 編譯時(shí)將生成 _Impl 實(shí)現(xiàn)類,此處將生成 MovieDataBase_Impl.java 文件

組件調(diào)用

本 demo 將結(jié)合 ViewModelRoom 進(jìn)行數(shù)據(jù)交互骂维,依賴 LiveData 進(jìn)行異步查詢惹资,畫(huà)面上則采用 Databinding 將數(shù)據(jù)和視圖自動(dòng)綁定。

class DemoActivity : AppCompatActivity() {
    private var movieViewModel: MovieViewModel? = null
    private var binding: ActivityRoomDbBinding? = null
    private var movieList: List<Movie?>? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityRoomDbBinding.inflate(layoutInflater)
        setContentView(binding!!.root)
        binding!!.lifecycleOwner = this

        movieViewModel = ViewModelProvider(this).get(MovieViewModel::class.java)
        movieViewModel?.getMovieList(this, { movieList: List<Movie?>? ->
            if (movieList == null) return@getMovieList
            this.movieList = movieList
            binding?.setMovieList(movieList)
        })
    }
}

ViewModel 通過(guò) MediatorLiveData 擔(dān)當(dāng)列表查詢的中介航闺,當(dāng) DB 初始化結(jié)束后再更新 UI布轿。

    private val mediatorLiveData = MediatorLiveData<List<Movie?>?>()
    private val db: MovieDataBase?
    private val mContext: Context

    init {
        mContext = application
        db = MovieDataBase.getInstance(mContext)
        if (db != null) {
            mediatorLiveData.addSource(db.movieDao().allMovies) { movieList ->
                if (db.databaseCreated.value != null) {
                    mediatorLiveData.postValue(movieList)
                }
            }
        };
    }

    fun getMovieList(owner: LifecycleOwner?, observer: Observer<List<Movie?>?>?) {
        if (owner != null && observer != null)
            mediatorLiveData.observe(owner, observer)
    }
}

RoomDatabase 創(chuàng)建后異步插入初始化數(shù)據(jù),并通知 MediatorLiveData来颤。

    val databaseCreated = MutableLiveData<Boolean?>()
    ...

    companion object {
        ...
        private fun createInstance(context: Context): MovieDataBase {
            return Room.databaseBuilder(context.applicationContext, ...)
                    ...
                    .addCallback(object : Callback() {
                        override fun onCreate(db: SupportSQLiteDatabase) {
                            super.onCreate(db)
                            Executors.newFixedThreadPool(5).execute {
                                val dataBase = getInstance(context)
                                val ids = dataBase!!.movieDao().insert(*Utils.initData)
                                dataBase.databaseCreated.postValue(true)
                            }
                        }
                        ...
                    })
                    .build()
        }
    }
}

運(yùn)行效果

Demo效果.png

通過(guò) Database Inspector 工具可以看到 DB 數(shù)據(jù)創(chuàng)建成功了汰扭。Database Inspector 支持實(shí)時(shí)刷新,查詢和修改等 DB 操作福铅,是 DB 開(kāi)發(fā)的利器萝毛。

如果不知道如何使用Database Inspector,可參考官方文檔使用 Database Inspector 調(diào)試數(shù)據(jù)庫(kù)

查看數(shù)據(jù)庫(kù).png

DAO 的具體使用

@Insert

@Insert 支持設(shè)置沖突策略滑黔,默認(rèn)為 OnConflictStrategy.ABORT 即中止并回滾笆包。還可以指定為其他策略。

  • OnConflictStrategy.REPLACE 沖突時(shí)替換為新記錄
  • OnConflictStrategy.IGNORE 忽略沖突 (不推薦)
  • OnConflictStrategy.ROLLBACK 已廢棄略荡,使用 ABORT 替代
  • OnConflictStrategy.FAIL 同上

其聲明的方法返回值可為空庵佣,也可為插入行的 ID 或列表。

  • fun insertWithOutId(movie: Movie?)
  • fun insert(movie: Movie?): Long?
  • fun insert(vararg movies: Movie?): LongArray?
@Delete

@Insert 一樣支持不返回刪除結(jié)果或返回刪除的函數(shù)汛兜,不再贅述巴粪。

@ Update

@Insert 一樣支持設(shè)置沖突策略和定制返回更新結(jié)果。此外需要注意的是 @Update 操作將匹配參數(shù)的主鍵 id 去更新字段。

  • fun update(vararg movies: Movie?): Int
@ Query

查詢操作主要依賴 @Update 的 value肛根,指定不同的 SQL 語(yǔ)句即可獲得相應(yīng)的查詢結(jié)果辫塌。在編譯階段就將驗(yàn)證語(yǔ)句是否正確,避免錯(cuò)誤的查詢語(yǔ)句影響到運(yùn)行階段派哲。

  • 查詢所有字段
    @get:Query(“SELECT * FROM movie”)

  • 查詢指定字段
    @get:Query(“SELECT id, movie_name, actor_name, post_year, review_score FROM movie”)

  • 排序查詢
    @get:Query(“SELECT * FROM movie ORDER BY post_year DESC”) 比如查詢最近發(fā)行的電影列表

  • 匹配查詢
    @Query(“SELECT * FROM movie WHERE id = :id”)

  • 多字段匹配查詢
    @Query(“SELECT * FROM movie WHERE movie_name LIKE :keyWord " + " OR actor_name LIKE :keyWord”) 比如查詢名稱和演員中匹配關(guān)鍵字的電影

  • 模糊查詢
    @Query(“SELECT * FROM movie WHERE movie_name LIKE ‘%’ || :keyWord || ‘%’ " + " OR actor_name LIKE ‘%’ || :keyWord || ‘%’”) 比如查詢名稱和演員中包含關(guān)鍵字的電影

  • 限制行數(shù)查詢
    @Query(“SELECT * FROM movie WHERE movie_name LIKE :keyWord LIMIT 3”) 比如查詢名稱匹配關(guān)鍵字的前三部電影

  • 參數(shù)引用查詢
    @Query(“SELECT * FROM movie WHERE review_score >= :minScore”) 比如查詢?cè)u(píng)分大于指定分?jǐn)?shù)的電影

  • 多參數(shù)查詢
    @Query(“SELECT * FROM movie WHERE post_year BETWEEN :minYear AND :maxYear”) 比如查詢介于發(fā)行年份區(qū)間的電影

  • 不定參數(shù)查詢
    @Query(“SELECT * FROM movie WHERE movie_name IN (:keyWords)”)

  • Cursor 查詢
    @Query(“SELECT * FROM movie WHERE movie_name LIKE ‘%’ || :keyWord || ‘%’ LIMIT :limit”)
    fun searchMoveCursorByLimit(keyWord: String?, limit: Int): Cursor?
    注意: Cursor 需要保證查詢到的字段和取值一一對(duì)應(yīng)臼氨,所以不推薦使用

  • 響應(yīng)式查詢
    demo 采用的 LiveData 進(jìn)行的觀察式查詢,還可以配合 RxJava2芭届,Kotlin 的 Flow 進(jìn)行響應(yīng)式查詢储矩。

進(jìn)階使用

數(shù)據(jù)庫(kù)升級(jí)降級(jí)

在 Movie 類里增加了新字段后,重新運(yùn)行已創(chuàng)建過(guò) DB 的 demo 會(huì)發(fā)生崩潰褂乍。

Room cannot verify the data integrity. Looks like you've changed schema but forgot to update the version number.

@Database 的 version 升級(jí)為 2 之后再次運(yùn)行仍然發(fā)生崩潰椰苟。

A migration from 1 to 2 was required but not found. Please provide the necessary Migration path via RoomDatabase.Builder.addMigration(Migration ...) or allow fallback of the RoomDatabase.Builder.fallbackToDestructiveMigration* methods.

提醒我們調(diào)用 fallbackToDestructiveMigration() 以允許升級(jí)失敗時(shí)破壞性地刪除 DB。

如果照做的話树叽,將能避免發(fā)生崩潰舆蝴,并且 onDestructiveMigration() 將被回調(diào)。在這個(gè)回調(diào)里可以試著重新初始化 DB题诵。

private fun createInstance(context: Context): MovieDataBase {
    return Room.databaseBuilder(context.applicationContext, MovieDataBase::class.java, DATA_BASE_NAME)
            .fallbackToDestructiveMigration()
            .addCallback(object : Callback() {
                override fun onDestructiveMigration(db: SupportSQLiteDatabase) {
                    super.onDestructiveMigration(db)
                    // Init DB again after db removed.
                    Executors.newFixedThreadPool(5).execute {
                        val dataBase = getInstance(context)
                        val ids = dataBase!!.movieDao().insert(*Utils.initData)
                        dataBase.databaseCreated.postValue(true)
                    }
                }
            })
            .build()
}

但是 DB 升級(jí)后洁仗,無(wú)論原有數(shù)據(jù)被刪除還是重新初始化都是用戶難以接受的。

我們可以通過(guò) addMigrations() 指定升級(jí)之后的遷移處理來(lái)達(dá)到保留舊數(shù)據(jù)和增加新字段的雙贏性锭。

比如如下展示的從版本 1 升級(jí)到版本 2赠潦,并增加一個(gè)默認(rèn)值為 8.0 的評(píng)分列的遷移處理。

private fun createInstance(context: Context): MovieDataBase {
    return Room.databaseBuilder(context.applicationContext, MovieDataBase::class.java, DATA_BASE_NAME)
            // .fallbackToDestructiveMigration()
            .addMigrations(object : Migration(1, 2) {
                override fun migrate(database: SupportSQLiteDatabase) {
                    database.execSQL("ALTER TABLE movie "
                            + " ADD COLUMN review_score INTEGER NOT NULL DEFAULT 8.0")
                }
            })
            ...
            })
            .build()
}

注意:

  • 降級(jí)則調(diào)用:
    fallbackToDestructiveMigrationOnDowngrade() 來(lái)指定在降級(jí)時(shí)刪除 DB草冈,也可以像上述那樣指定 drop column 來(lái)進(jìn)行數(shù)據(jù)遷移她奥。
  • 如果想要遷移數(shù)據(jù),無(wú)論是升級(jí)還是降級(jí)怎棱,必須要給 @Database 的 version 指定正確的目標(biāo)版本哩俭。Migration 遷移處理的起始版本以及實(shí)際的遷移處理 migrate() 都必不可少。
事務(wù)處理

當(dāng)我們的 DB 操作需要保持一致性拳恋,或者查詢關(guān)聯(lián)性結(jié)果的時(shí)候需要保證事務(wù)處理凡资。Room 提供了 @Transaction 注解幫助我們快速實(shí)現(xiàn)這個(gè)需求,它將確保注解內(nèi)的方法運(yùn)行在同一個(gè)事務(wù)模式谬运。

@Dao
public interface MovieDao {
    @Transaction
    default void insetNewAndDeleteOld(Movie newMovie, Movie oldMovie) {
        insert(newMovie);
        delete(oldMovie);
    }
}

需要注意的是隙赁,事務(wù)處理比較占用性能,避免在事務(wù)處理的方法內(nèi)執(zhí)行耗時(shí)邏輯梆暖。

另外伞访,@Inset@Delete@Update 的處理自動(dòng)在事務(wù)模式進(jìn)行處理轰驳,無(wú)需增加 @Transaction 注解厚掷。

public long[] insert(final Movie... movies) {
    __db.assertNotSuspendingTransaction();
    __db.beginTransaction();
    try {
        long[] _result = __insertionAdapterOfMovie.insertAndReturnIdsArray(movies);
        __db.setTransactionSuccessful();
        return _result;
    } finally {
        __db.endTransaction();
    }
}

上面的源碼也啟發(fā)我們可以手動(dòng)執(zhí)行事務(wù)處理弟灼,一般來(lái)說(shuō)不需要,取決于具體情況蝗肪。

RoomDatabase 的 beginTransaction() 和 endTransaction() 不推薦外部使用了,可以采用封裝好的 runInTransaction() 實(shí)現(xiàn)蠕趁。

db.runInTransaction(Runnable {
    val database = db.getOpenHelper().getWritableDatabase();

    val contentValues = ContentValues()
    contentValues.put("movie_name", newMovie.getName())
    contentValues.put("actor_name", newMovie.getActor())
    contentValues.put("post_year", newMovie.getYear())
    contentValues.put("review_score", newMovie.getScore())

    database.insert("movie", SQLiteDatabase.CONFLICT_ABORT, contentValues)
    database.delete("movie", "id = " + oldMovie.getId(), null)
})

原理淺談

簡(jiǎn)要介紹下 Room 的部分實(shí)現(xiàn)原理薛闪。因篇幅有限只展示關(guān)鍵流程,感興趣者可自行探究具體代碼俺陋。

RoomDatabase 的創(chuàng)建

RoomDatabase$Builder 的 build() 調(diào)用后便通過(guò)反射創(chuàng)建了 @Databse 注解聲明的 RoomDatabase 實(shí)例 XXX_Impl豁延。

RoomDatabase的創(chuàng)建.png

SupportSQLiteDatabase 的創(chuàng)建

SupportSQLiteDatabase 是模仿 SQLiteDatabase 作成的接口,供 Room 框架內(nèi)部對(duì) DB 進(jìn)行操作腊状。由 FrameworkSQLiteDatabase 實(shí)現(xiàn)诱咏,其將通過(guò)內(nèi)部持有的 SQLiteDatabase 實(shí)例,代理 DB 操作缴挖。

SupportSQLiteDatabase 的創(chuàng)建由增刪改查等 DB 操作觸發(fā)袋狞,需要經(jīng)歷 DB 的創(chuàng)建,表的創(chuàng)建映屋,表的初始化苟鸯,升降級(jí)以及打開(kāi)等過(guò)程。

創(chuàng)建 DB 文件
創(chuàng)建DB文件.png
創(chuàng)建表
創(chuàng)建表.png
初始化表
初始化表.png
升級(jí)表

DB 文件已經(jīng)存在并且版本和目標(biāo)版本不一致的話棚点,將執(zhí)行數(shù)據(jù)遷移早处。但如果遷移處理未配置或者執(zhí)行失敗了便將刪除 DB 并執(zhí)行相應(yīng)的回調(diào)。

升級(jí)表.png
打開(kāi)表

DB 的創(chuàng)建或升級(jí)都正常完成后將回調(diào) onOpen()瘫析。

打開(kāi)表.png

注意

Room 框架的使用過(guò)程中遇到了些容易出錯(cuò)的地方砌梆,需要格外留意。

  • RoomDatabase 的實(shí)例建議采用單例模式管理
  • 不要在 UI 線程執(zhí)行 DB 操作贬循,否則發(fā)生異常:
    Cannot access database on the main thread since it may potentially lock the UI for a long period of time.
    通過(guò)調(diào)用:
    allowMainThreadQueries() 可以回避咸包,但不推薦。
  • 不要在 Callback#onCreate() 里同步執(zhí)行 insert 等 DB 處理杖虾,否則將阻塞 DB 實(shí)例的初始化并發(fā)生異常: getDatabase called recursively诉儒。
  • @Entity 注解類不要提供多個(gè)構(gòu)造函數(shù),使用 @Ignore 可以回避亏掀。
  • Callback#onCreate() 并非由 RoomDatabase$Builder#build() 觸發(fā)忱反,而是由具體的增刪改查操作觸發(fā),切記滤愕。

結(jié)語(yǔ)

通過(guò)上述的實(shí)戰(zhàn)和原理介紹可以看出温算,Room 的本質(zhì)是在 SQLite 的基礎(chǔ)上進(jìn)行封裝的抽象層,通過(guò)一系列注解讓用戶能夠更簡(jiǎn)便的使用 SQLite间影。正因?yàn)榇俗⒏停邆淞艘恍﹥?yōu)勢(shì),值得開(kāi)發(fā)者大膽使用。

  • 聲明注解便能完成接口的定義巩割,易上手

  • 編譯階段將驗(yàn)證注解里聲明的 SQL 語(yǔ)句裙顽,提高了開(kāi)發(fā)效率

  • 支持使用 RxJava2,LiveData 以及 Flow 進(jìn)行異步查詢

  • 相較其他數(shù)據(jù)庫(kù)框架 SQL 執(zhí)行效率更高

Demo參見(jiàn)Github-JetpackDemo

參考資料

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末愈犹,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子闻丑,更是在濱河造成了極大的恐慌漩怎,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,123評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件嗦嗡,死亡現(xiàn)場(chǎng)離奇詭異勋锤,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)侥祭,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門叁执,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人矮冬,你說(shuō)我怎么就攤上這事徒恋。” “怎么了欢伏?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,723評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵入挣,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我硝拧,道長(zhǎng)径筏,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,357評(píng)論 1 283
  • 正文 為了忘掉前任障陶,我火速辦了婚禮滋恬,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘抱究。我一直安慰自己恢氯,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,412評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布鼓寺。 她就那樣靜靜地躺著勋拟,像睡著了一般。 火紅的嫁衣襯著肌膚如雪妈候。 梳的紋絲不亂的頭發(fā)上敢靡,一...
    開(kāi)封第一講書(shū)人閱讀 49,760評(píng)論 1 289
  • 那天,我揣著相機(jī)與錄音苦银,去河邊找鬼啸胧。 笑死赶站,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的纺念。 我是一名探鬼主播贝椿,決...
    沈念sama閱讀 38,904評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼陷谱!你這毒婦竟也來(lái)了烙博?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,672評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤叭首,失蹤者是張志新(化名)和其女友劉穎习勤,沒(méi)想到半個(gè)月后踪栋,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體焙格,經(jīng)...
    沈念sama閱讀 44,118評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,456評(píng)論 2 325
  • 正文 我和宋清朗相戀三年夷都,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了眷唉。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,599評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡囤官,死狀恐怖冬阳,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情党饮,我是刑警寧澤肝陪,帶...
    沈念sama閱讀 34,264評(píng)論 4 328
  • 正文 年R本政府宣布,位于F島的核電站刑顺,受9級(jí)特大地震影響氯窍,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜蹲堂,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,857評(píng)論 3 312
  • 文/蒙蒙 一狼讨、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧柒竞,春花似錦政供、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,731評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至稼虎,卻和暖如春执泰,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背渡蜻。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,956評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工术吝, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留计济,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,286評(píng)論 2 360
  • 正文 我出身青樓排苍,卻偏偏與公主長(zhǎng)得像沦寂,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子淘衙,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,465評(píng)論 2 348

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