Entity Framework Core 5中實現(xiàn)批量更新、刪除

本文介紹了一個在Entity Framework Core 5中不需要預(yù)先加載數(shù)據(jù)而使用一句SQL語句批量更新儒拂、刪除數(shù)據(jù)的開發(fā)包桌肴,并且分析了其實現(xiàn)原理皇筛,并且與其他實現(xiàn)方案做了比較。

一坠七、背景

隨著微軟全面擁抱開源水醋,.Net開源社區(qū)百花開放,涌現(xiàn)了非常多優(yōu)秀的開源彪置,ORM項目就有Dapper拄踪、SqlSugar、PetaPoco拳魁、FreeSQL等惶桐。作為微軟官方提供的ORM框架,Entity Framework Core(以下簡稱EF Core)顯然是被關(guān)注最多的潘懊。EF Core非常優(yōu)秀而且功能豐富姚糊,但是EF Core有一個一直被人詬病的地方就是它并不能很好支持?jǐn)?shù)據(jù)的批量更新和批量刪除。在EF Core中批量更新和刪除數(shù)據(jù)都要先把數(shù)據(jù)加載到內(nèi)存中授舟,然后再對數(shù)據(jù)操作救恨,最后再SaveChanges,比如下面的代碼用于把所有Id大于2或者AuthorName中含有”zack”的價格增加3:

var books2 = ctx.Books.Where(b => b.Id >

2||b.AuthorName.Contains("zack"));

foreach(var b in books2)

{

??? b.Price =b.Price + 3;

}

ctx.SaveChanges();

讓我們查看上面的程序幕后執(zhí)行的SQL語句:

可以看到释树,EF Core先把數(shù)據(jù)用Select查詢出來肠槽,然后在內(nèi)存中逐個修改擎淤,最后再把被修改對象每個都執(zhí)行一次Update語句去更新。

再比如秸仙,如下的代碼用于刪除Price大于5元的記錄:

var books1 = ctx.Books.Where(b => b.Price > 5);

ctx.RemoveRange(books1);

ctx.SaveChanges();

讓我們查看上面的程序運(yùn)行幕后執(zhí)行的SQL語句:

可以看到嘴拢,EF Core先把數(shù)據(jù)用Select查詢出來,然后再對每條記錄都執(zhí)行Delete語句去刪除筋栋。

很顯然炊汤,如果批量更新或者刪除的數(shù)據(jù)量比較大,這樣的操作性能是非常低的弊攘。

因此抢腐,我們需要一種在EF Core中使用一條SQL語句就高性能地刪除或者更新數(shù)據(jù)的方法。

二襟交、為什么微軟不提供這樣的方法

盡管用戶的要求強(qiáng)烈迈倍,但是微軟一直沒有提供高效的批量刪除和更新的方式。在EF Core Github的issue中?[1]捣域,微軟給出的理由是:這樣做會導(dǎo)致EF Core的對象狀態(tài)跟蹤混亂啼染,比如對于同一個DbContext,如果用批量刪除的方法刪除了數(shù)據(jù)焕梅,那么在被刪除之前查詢出來的數(shù)據(jù)狀態(tài)就混亂了迹鹅,因此需要重構(gòu)EF Core的代碼,工作量比較大贞言。

作為一個成熟的框架斜棚,考慮這些邏輯問題以避免潛在的風(fēng)險是有必要的,是可以理解的该窗。但是作為實際的開發(fā)者弟蚀,我們是有辦法規(guī)避這些問題的。比如一般的Web應(yīng)用中酗失,刪除操作都是在一個單獨的Http請求進(jìn)行中的义钉,因此不涉及到微軟擔(dān)心的問題。即使在有的場景下规肴,涉及到在通過同一個DbContext在數(shù)據(jù)刪除之前就把數(shù)據(jù)查詢出來的場景捶闸,那么也完全可以通過在刪除之后再查一次的方式來規(guī)避這個問題。

根據(jù)github上那個issue的回復(fù)奏纪,微軟有考慮在EF Core 6.0中加入高效地批量刪除和更新數(shù)據(jù)的方式鉴嗤,但是僅僅是“考慮”,并不確定序调。我們作為普通開發(fā)者可等不及了醉锅,因此要自己去解決。

三发绢、已有解決方法

有如下三種已有的解決方法:

1.? 執(zhí)行原生SQL語句硬耍。在EF Core中提供了ctx.Database.ExecuteSqlRaw()等方法可以用來執(zhí)行原生SQL語句垄琐,因此我們可以直接編寫Delete、Update語句來刪除或者更新數(shù)據(jù)经柴。這種方式比較直接狸窘,缺點就是這樣代碼中直接操作數(shù)據(jù)表的方式不太符合模型驅(qū)動、分層隔離等思想坯认,程序員直接面對數(shù)據(jù)庫表翻擒,無法利用EF Core強(qiáng)類型的特性,如果模型發(fā)生改變牛哺,必須手動變更SQL語句陋气;而且如果調(diào)用了一些DBMS特有的語法、函數(shù)引润,一旦程序遷移到其他DBMS巩趁,就可能要重新編寫SQL語句,而無法利用EF Core強(qiáng)大的SQL翻譯機(jī)制來屏蔽不同底層數(shù)據(jù)庫的差異淳附。

2.使用其他ORM议慰。FreeSQL等ORM中提供了批量Delete、Update語句的方法奴曙,使用也非常簡單别凹。這種方式的缺點是項目中必須引入第三方的ORM,無法復(fù)用EF Core的代碼洽糟。

3. 使用已有的EF Core擴(kuò)展番川。EF Plus、EFCore.BulkExtensions等開源庫中都提供了在EF Core框架下進(jìn)行批量操作的方法脊框。實現(xiàn)這個的核心就是要獲得EF Core生成的SQL語句以及SelectExpression。由于EF Core 5.0之前的版本中沒有提供公開的API用于獲取一個LINQ操作對應(yīng)的SQL語句践啄,所以這些開源庫都是通過訪問EF Core框架中一些類的私有成員來完成的獲取LINQ對應(yīng)的SQL語句以及SelectExpression的方法?[2]浇雹。由于用的是訪問私有成員這樣不符合面向?qū)ο笤瓌t的方式,所以一旦EF Core框架代碼發(fā)生改變屿讽,代碼就可能會失敗昭灵,之前就發(fā)生過EF Core新版本發(fā)布造成這些開源庫無法工作的情況。而且伐谈,在撰寫這篇文章的時候烂完,這些開源庫還沒有適配.Net 5。

四诵棵、我的實現(xiàn)Zack.EFCore.Batch

我開發(fā)了一個Entity Framework Core的擴(kuò)展庫抠蚣,讓開發(fā)者在Entity Framework Core中可以用一句SQL進(jìn)行數(shù)據(jù)的刪除或者更新。由于開發(fā)中用到了Entity Framework Core 5的API履澳,所以這個庫要求Entity Framework Core 5及以上版本嘶窄,也就是.Net 5及以上版本怀跛。

下面介紹一下使用方法:

第一步,通過Nuget安裝Install-Package Zack.EFCore.Batch

第二步柄冲,把如下代碼添加到你的DbContext類的OnConfiguring方法中:

optionsBuilder.UseBatchEF();

第三步: 使用DbContext的擴(kuò)展方法DeleteRangeAsync()來刪除一批數(shù)據(jù). DeleteRangeAsync()的參數(shù)就是過濾條件的lambda表達(dá)式吻谋。

批量刪除的例子代碼如下:

await ctx.DeleteRangeAsync<Book>(b =>

b.Price > n || b.AuthorName == "zack yang");

上面的代碼將會在數(shù)據(jù)庫中執(zhí)行如下SQL語句:

Delete FROM [T_Books] WHERE ([Price] > @__p_0) OR

([AuthorName] = @__s_1)

DeleteRange()方法是DeleteRangeAsync()的同步方法版本。

使用DbContext的擴(kuò)展方法BatchUpdate()來創(chuàng)建一個BatchUpdateBuilder對象现横。 BatchUpdateBuilder類有如下四個方法:

1)? Set()方法用于給一個屬性賦值漓拾。方法的第一個參數(shù)是屬性的lambda表達(dá)式,第二個參數(shù)是值的lambda表達(dá)式。

2) Where() 是過濾條件

3)? ExecuteAsync()使用用于執(zhí)行BatchUpdateBuilder的異步方法

4)? Execute()是ExecuteAsync()的同步方法版本戒祠。

例子代碼:

await ctx.BatchUpdate<Book>()

?? .Set(b =>b.Price, b => b.Price + 3)

?? .Set(b =>b.Title, b => s)

??.Set(b=>b.AuthorName,b=>b.Title.Substring(3,2)+b.AuthorName.ToUpper())

?? .Set(b =>b.PubTime, b => DateTime.Now)

?? .Where(b=> b.Id > n || b.AuthorName.StartsWith("Zack"))

??.ExecuteAsync();

上面的代碼將會在SQLServer數(shù)據(jù)庫中執(zhí)行如下SQL語句:

Update [T_Books] SET [Price] = [Price] + 3.0E0,

[Title] = @__s_1, [AuthorName] = COALESCE(SUBSTRING([Title], 3 + 1, 2), N'') +

COALESCE(UPPER([AuthorName]), N''), [PubTime] = GETDATE()

WHERE ([Id] > @__p_0) OR ([AuthorName] IS NOT NULL

AND ([AuthorName] LIKE N'Zack%'))

這個開發(fā)包使用EF Core實現(xiàn)的lambda表達(dá)式到SQL語句的翻譯骇两,所以幾乎所有EF Core支持的lambda表達(dá)式寫法都被支持。

項目的GitHub地址:https://github.com/yangzhongke/Zack.EFCore.Batch

五得哆、實現(xiàn)原理分析

其實要把lambda表達(dá)式轉(zhuǎn)換為SQL語句并不難脯颜,只要對表達(dá)式樹進(jìn)行解析就可以生成SQL語句,但是最難的部分是對于.Net函數(shù)到SQL片段的翻譯贩据,因為相同的.Net函數(shù)在不同DBMS中等效的SQL片段是不同的栋操,如果我自己實現(xiàn)這個是很麻煩的,因此我想到了直接借用EF Core的表達(dá)式樹到SQL語句的翻譯引擎來實現(xiàn)是最佳的方法饱亮。

不幸的是矾芙,在.Net Core 3.x及之前,是無法直接獲取一個Linq查詢翻譯后的SQL語句的近上。.Net Core中可以通過日志等方式獲取翻譯后的SQL語句剔宪,但是這些都是Linq執(zhí)行后才能獲得的,而且是無法在拿到一個Lambda表達(dá)式或者IQueryable的時候立即獲得SQL的壹无。經(jīng)過詢問.Net Core開發(fā)團(tuán)隊得知葱绒,在.Net Core 3.X及之前,也是沒有公開的API可以完成表達(dá)式樹到SQL片段翻譯的功能斗锭。

從.Net 5開始泼差,Entity Framework Core 中提供了不用執(zhí)行查詢免姿,就可以直接獲取Linq查詢對應(yīng)的SQL語句的方法伪朽,那就是調(diào)用IQueryable的ToQueryString()方法?[3]溜宽。

因此我就想通過這個ToQueryString()方法拿到的SQL語句來入手來實現(xiàn)這個功能。 可以把用到的Lambda表達(dá)式片段豺撑、過濾表達(dá)式拼接到一個查詢表達(dá)式中烈疚,然后調(diào)用ToQueryString()方法獲取翻譯后的SQL語句,然后編寫詞法分析器和語法分析器對SQL語句進(jìn)行分析聪轿,提取出Where子句以及Select列中的表達(dá)式片段爷肝,然后再把這些片段重新組合成Update、Delete的SQL語句即可。

不過阶剑,由于不同DBMS的語法不同跃巡,編寫這樣的詞法及語法分析器是很麻煩的,我就想能否研究ToQueryString()的實現(xiàn)原理牧愁,然后直接拿到解析過程中的SQL片段素邪,這樣就避免了生成SQL后再去解析的工作。

雖然EF Core是開源的猪半,不過由于關(guān)于EF Core的源代碼并沒有一個全面介紹的文檔兔朦,而EF Core的代碼又是非常復(fù)雜的,所以研究EF Core的源代碼是非常耗時的磨确。研究過程中沽甥,我?guī)状味枷胍艞墸詈蠼K于把功能實現(xiàn)了乏奥,通過開發(fā)這個庫摆舟,我也對于EF Core的內(nèi)部原理,特別是從Lambda表達(dá)式到SQL的翻譯的整個過程了解的非常透徹邓了。我這里不對研究的過程去回顧恨诱,而是直接為大家講解一下EF

Core的原理,然后再講解一下我這個Zack.EFCore.Batch的實現(xiàn)原理骗炉。

1.? EF Core的SQL翻譯原理

EF Core中有很多的服務(wù)照宝,比如對于IQueryable進(jìn)行預(yù)處理的QueryTranslationPreprocessor、從查詢中提取查詢參數(shù)的RelationalParameterBasedSqlProcessor句葵、把表達(dá)式樹翻譯為SQL語句的QuerySqlGenerator等厕鹃。這些服務(wù)一般都是通過IXXX Factory這樣的工廠類的Create()方法創(chuàng)建的,比如QueryTranslationPreprocessor對應(yīng)的IQueryTranslationPreprocessorFactory乍丈、QuerySqlGenerator對應(yīng)的IQuerySqlGeneratorFactory剂碴。而這些工廠類的對象則是通過dbContext.GetService<XXX>()來從DbContext中獲得的。當(dāng)然轻专,也有的服務(wù)是不需要通過工廠直接獲得的汗茄,比如Lambda編譯器服務(wù)IQueryCompiler就可以直接通過ctx.GetService<IQueryCompiler>()獲取。

因此铭若,如果你想使用EF Core中其他的服務(wù),都可以嘗試把對應(yīng)的服務(wù)接口類型或者工廠類型放到GetService()中查詢一下試試递览。

EF Core中還允許調(diào)用DbContextOptionsBuilder的ReplaceService()方法把EF Core中的默認(rèn)服務(wù)替換為自定義實現(xiàn)類叼屠。

EF Core中把一個IQueryable對象翻譯為SQL語句的代碼分散在各個類中,我經(jīng)過努力绞铃,把它們整合為一段可以運(yùn)行的代碼镜雨,如下:

Expression query = queryable.Expression;

var databaseDependencies =

ctx.GetService<DatabaseDependencies>();

IQueryTranslationPreprocessorFactory

_queryTranslationPreprocessorFactory = ctx.GetService<IQueryTranslationPreprocessorFactory>();

IQueryableMethodTranslatingExpressionVisitorFactory

_queryableMethodTranslatingExpressionVisitorFactory =

ctx.GetService<IQueryableMethodTranslatingExpressionVisitorFactory>();

IQueryTranslationPostprocessorFactory

_queryTranslationPostprocessorFactory =

ctx.GetService<IQueryTranslationPostprocessorFactory>();

QueryCompilationContext queryCompilationContext =

databaseDependencies.QueryCompilationContextFactory.Create(true);


IDiagnosticsLogger<DbLoggerCategory.Query>

logger = ctx.GetService<IDiagnosticsLogger<DbLoggerCategory.Query>>();

QueryContext queryContext =

ctx.GetService<IQueryContextFactory>().Create();

QueryCompiler queryComipler =

ctx.GetService<IQueryCompiler>() as QueryCompiler;

//parameterize determines if it will use "Declare"

or not

MethodCallExpression methodCallExpr1 =

queryComipler.ExtractParameters(query, queryContext, logger, parameterize:

true) as MethodCallExpression;

QueryTranslationPreprocessor

queryTranslationPreprocessor = _queryTranslationPreprocessorFactory.Create(queryCompilationContext);

MethodCallExpression methodCallExpr2 =

queryTranslationPreprocessor.Process(methodCallExpr1) as MethodCallExpression;

QueryableMethodTranslatingExpressionVisitor

queryableMethodTranslatingExpressionVisitor =

?????? _queryableMethodTranslatingExpressionVisitorFactory.Create(queryCompilationContext);

ShapedQueryExpression shapedQueryExpression1 =

queryableMethodTranslatingExpressionVisitor.Visit(methodCallExpr2) as

ShapedQueryExpression;

QueryTranslationPostprocessor queryTranslationPostprocessor=

_queryTranslationPostprocessorFactory.Create(queryCompilationContext);

ShapedQueryExpression shapedQueryExpression2 =

queryTranslationPostprocessor.Process(shapedQueryExpression1) as

ShapedQueryExpression;


IRelationalParameterBasedSqlProcessorFactory

_relationalParameterBasedSqlProcessorFactory =

?????? ctx.GetService();

RelationalParameterBasedSqlProcessor

_relationalParameterBasedSqlProcessor =

_relationalParameterBasedSqlProcessorFactory.Create(true);


SelectExpression selectExpression =

(SelectExpression)shapedQueryExpression2.QueryExpression;

selectExpression =

_relationalParameterBasedSqlProcessor.Optimize(selectExpression,

queryContext.ParameterValues, out bool canCache);

IQuerySqlGeneratorFactory querySqlGeneratorFactory =

ctx.GetService<IQuerySqlGeneratorFactory>();

QuerySqlGenerator querySqlGenerator =

querySqlGeneratorFactory.Create();

var cmd =

querySqlGenerator.GetCommand(selectExpression);

string sql = cmd.CommandText;

大致解釋一下上面的代碼:

queryable是一個待轉(zhuǎn)換的IQueryable對象,ctx是一個DbContext對象儿捧。QueryCompilationContext是Lambda到SQL翻譯這個“編譯”過程的上下文荚坞,很多工廠類的Create方法都要用它做參數(shù)挑宠。QueryContext是查詢語句的上下文。SelectExpression是Linq查詢的表達(dá)式樹翻譯為強(qiáng)類型的抽象語法樹的樹根颓影。QuerySqlGenerator的GetCommand()方法用于遍歷SelectExpression生成目標(biāo)SQL語句各淀。

QuerySqlGenerator的GetCommand方法最終會調(diào)用VisitSelect(SelectExpression

selectExpression)來拼接生成SQL語句,其中會調(diào)用VisitSqlBinary(SqlBinaryExpression sqlBinaryExpression)诡挂、VisitFromSql(FromSqlExpression fromSqlExpression)碎浇、VisitLike(LikeExpression likeExpression)等方法來把運(yùn)算表達(dá)式、From璃俗、Like等翻譯成對應(yīng)的SQL片段奴璃。由于不同DBMS中一些函數(shù)等實現(xiàn)不同,而SelectExpression城豁、LikeExpression等都是一個抽象節(jié)點苟穆,是獨立于具體DBMS的抽象模型,因此各個DBMS的EF Provider只要負(fù)責(zé)編寫代碼把這些XXExpression翻譯為各自的SQL片段即可唱星,不同DBMS的EF Core中的代碼大部分都是各種XXTranslatorProvider雳旅。

2. Zack.EFCore.Batch的實現(xiàn)原理

這個庫最核心的代碼就是ZackQuerySqlGenerator,它是一個繼承自QuerySqlGenerator的類魏颓。它通過override父類的VisitSelect方法岭辣,然后把父類的VisitSelect方法的代碼全部拷過來。這樣的目的就是在VisitSelect拼接SQL語句的過程中把各個SQL片段截獲到甸饱。以下面的代碼為例:

if (selectExpression.Predicate != null)

{

?????? Sql.AppendLine().Append("WHERE");

?????? varoldSQL = Sql.Build().CommandText;//zack's code

?????? Visit(selectExpression.Predicate);

?????? this.PredicateSQL= Diff(oldSQL, this.Sql.Build().CommandText); //zack's code

}

這里就是首先把拼接Where條件之前的SQL語句保存到oldSQL變量中沦童,再把拼接Where條件之后的SQL語句和oldSQL求一個差運(yùn)算,就得到了Where語句的SQL片段叹话。

然后通過optBuilder.ReplaceService<IQuerySqlGeneratorFactory,

ZackQuerySqlGeneratorFactory>();把ZackQuerySqlGenerator對應(yīng)的ZackQuerySqlGeneratorFactory替換為IQuerySqlGeneratorFactory的默認(rèn)實現(xiàn)偷遗。這樣EF Core再完成從SelectExpression到SQL語句的翻譯,就會使用ZackQuerySqlGenerator類驼壶,這樣我們就可以截獲翻譯生成的SQL片段了氏豌。

再解釋一下批量更新數(shù)據(jù)庫的BatchUpdateBuilder類的主要代碼。代碼主要就是把Age=Age+1,

Name=AuthorName.Trim()這樣的賦值表達(dá)式重新生成Select(new{b.Age,b.Age+1,b.Name,b.AuthorName.Trime()})這樣的表達(dá)式热凹,這樣就把N個賦值表達(dá)式重新拼接為2*N個查詢表達(dá)式泵喘,再把查詢條件拼接形成一個IQueryable對象,再調(diào)用ZackQuerySqlGenerator翻譯IQueryable獲取到Where的SQL片段以及各個列的SQL片段般妙,最后重新拼接成一個Update的SQL語句纪铺。

六、局限性

Zack.EFCore.Batch有如下局限性:

1.由于Zack.EFCore.Batch用到了EF Core 5.0的新API碟渺,所以暫不支持EF Core 3.X及以下版本鲜锚。

2. 由于Zack.EFCore.Batch是直接操作數(shù)據(jù)庫,所以更新、刪除后芜繁,會存在微軟擔(dān)心的同一個DbContext中已經(jīng)查詢出來的對象跟蹤狀態(tài)和數(shù)據(jù)庫不一致的情況旺隙。在同一個DbContext實例中,如果需要在批量刪除或者更新之后操作同一個DbContex中之前查詢出來的數(shù)據(jù)骏令,建議再執(zhí)行一遍查詢操作蔬捷。

3.代碼中使用了一個內(nèi)部API QueryCompiler,這是不推薦的做法伏社。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末抠刺,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子摘昌,更是在濱河造成了極大的恐慌速妖,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件聪黎,死亡現(xiàn)場離奇詭異罕容,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)稿饰,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進(jìn)店門锦秒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人喉镰,你說我怎么就攤上這事旅择。” “怎么了侣姆?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵生真,是天一觀的道長。 經(jīng)常有香客問我捺宗,道長柱蟀,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任蚜厉,我火速辦了婚禮长已,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘昼牛。我一直安慰自己术瓮,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布贰健。 她就那樣靜靜地躺著胞四,像睡著了一般。 火紅的嫁衣襯著肌膚如雪霎烙。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天,我揣著相機(jī)與錄音悬垃,去河邊找鬼游昼。 笑死,一個胖子當(dāng)著我的面吹牛尝蠕,可吹牛的內(nèi)容都是我干的烘豌。 我是一名探鬼主播,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼看彼,長吁一口氣:“原來是場噩夢啊……” “哼廊佩!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起靖榕,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤标锄,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后茁计,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體料皇,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年星压,在試婚紗的時候發(fā)現(xiàn)自己被綠了践剂。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡娜膘,死狀恐怖逊脯,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情竣贪,我是刑警寧澤军洼,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布,位于F島的核電站贾富,受9級特大地震影響歉眷,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜颤枪,卻給世界環(huán)境...
    茶點故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一汗捡、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧畏纲,春花似錦扇住、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至票灰,卻和暖如春女阀,著一層夾襖步出監(jiān)牢的瞬間宅荤,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工浸策, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留冯键,地道東北人。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓庸汗,卻偏偏與公主長得像惫确,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子蚯舱,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,877評論 2 345

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