Winform开发框架之存储过程的支持--存储过程的实现和演化提炼(1)

在我前面很多篇关于框架设计和介绍的文章里面,大多数都是利用框架提供的基础性API进行各种的操作,包括增删改查、分页等各种实现和其衍生的实现,而这些实现绝大多数是基于SQL的标准操作实现的,由于框架的底层是利用了微软企业库Enterprise Library,因此框架也是很好的支持存储过程的各种调用,不过由于整体性和数据库迁移方面的考虑,建议一般使用标准的SQL操作而已,这样能够很大程度上保证数据库可以很平滑过渡到其他数据库,如Access、SQLite等单机版数据库。但是,有时候我们提供对存储过程的支持也是十分必要的,有些业务可能就只是固定在某种特定的数据库上跑,如SQLServer、Oracle等这些支持存储过程的关系型数据库,有些业务可能还真的需要存储过程的整体性的封装;基于这个原因,我撰写了这篇文章,力求从较为全面的角度上阐述存储过程的编写、实现和演化提炼方面做一个介绍。

1、SQLServer存储过程的编写

虽然存储过程一般用于处理一些复杂的逻辑关系或者报表内容,不过为了介绍方便,我们从几个较为基础的操作进行介绍。
我们以一个客户表来进行对应的存储过程来介绍,先介绍客户表T_Customer的表定义。



它的SQLServer脚本如下所示

create table dbo.T_Customer (
   ID                   nvarchar(50)         not null,
   Name                 nvarchar(50)         null,
   Age                  int                  null,
   Creator              nvarchar(50)         null,
   CreateTime           datetime             null,
   constraint PK_T_CUSTOMER primary key (ID)
)

为了介绍存储过程的编写,我们以这个表的相关操作的存储过程来进行介绍,存储过程一般可以分为下面几种情况。
1)提供执行处理,可对执行结果进行反馈
这种情况常常可以见到,如可以对插入、更新、删除等操作进行处理,并获得执行的结果,下面是这两种存储过程的代码。

------------------------------------
--作者:伍华聪 http://wuhuacong.cnblogs.com
--创建时间:2014年11月27日 
--功能描述:插入数据到表中 
------------------------------------
 CREATE PROCEDURE dbo.T_Customer_Insert 
 ( 
      @ID varchar(50),
      @Name varchar(50) ,
      @Age int 
 ) 
 AS 
 begin tran 
 Insert into dbo.T_Customer( ID,Name,Age ) Values( @ID,@Name,@Age ) 
 if @@error!=0 
     begin 
         rollback 
     end 
 else 
     begin 
         commit 
     end 
 go 
 
------------------------------------
--作者:伍华聪 http://wuhuacong.cnblogs.com
--创建时间:2014年11月27日 
--功能描述:以字段ID为关键字,修改表中的数据 
------------------------------------
 CREATE PROCEDURE dbo.T_Customer_UpdateByID 
 ( 
      @ID varchar(50),
      @Name varchar(50) ,
      @Age int 
 ) 
 AS 
 begin tran 
 Update dbo.T_Customer Set Name=@Name,Age=@Age Where ID= @ID 
 if @@error!=0 
     begin 
         rollback 
     end 
 else 
     begin 
         commit 
     end 
 go 
------------------------------------
--作者:伍华聪 http://wuhuacong.cnblogs.com
--创建时间:2014年11月27日 
--功能描述:以字段ID为关键字,删除表的记录 
------------------------------------
 CREATE PROCEDURE dbo.T_Customer_DeleteByID 
 ( 
      @ID varchar(50)
 ) 
 AS 
 begin tran 
 Delete From dbo.T_Customer where ID=@ID 
 if @@error!=0 
     begin 
         rollback 
     end 
 else 
     begin 
         commit 
     end 
 go 

2)提供执行处理,获得一个或者多个返回性参数,并可对执行结果进行反馈。

基于上面的处理方式,我们可能还有一种情况,就是需要执行存储过程个,并返回对应的返回参数,我们可以在程序里面利用代码获取这些返回参数的数值,从而用作其他用途。

因此,这种操作,如要是获取返回性参数的情况,如下所示是判断记录是否存在,以及获取客户最大年龄的两个存储过程。

------------------------------------
--作者:伍华聪 http://wuhuacong.cnblogs.com
--创建时间:2014年11月27日 
--功能描述:以字段ID为关键字,检查表中是否存在符合条件的记录 
------------------------------------
 CREATE PROCEDURE dbo.T_Customer_ExistByID 
 ( 
     @Exist int output , 
      @ID varchar(50)
 ) 
 AS 
 Select @Exist = Case When Exists (Select 1 From dbo.T_Customer Where ID=@ID) Then 1 Else 0 End 
 go 

------------------------------------
--作者:伍华聪 http://wuhuacong.cnblogs.com
--创建时间:2014年11月27日 
--功能描述:获取客户最大年龄
------------------------------------
 CREATE PROCEDURE dbo.T_Customer_MaxAge
( @MaxAge int output )
 AS 
 Select @MaxAge=Case When Max(Age) is NULL Then 0 Else Max(Age) End  From dbo.T_Customer 
 go 

3)提供查询处理,并返回实体对象

这小节后面介绍的内容,都是存储过程的返回值,这些或者是一条记录,或者是多条记录的查询结果,这个在SQLServer里面很容易实现,而在Oracle里面需要通过游标进行处理。

下面存储过程脚本,是基于返回单条记录的存储过程。

------------------------------------
--作者:伍华聪 http://wuhuacong.cnblogs.com
--创建时间:2014年11月27日 
--功能描述:以字段ID为关键字,检索表中的数据 
------------------------------------
 CREATE PROCEDURE dbo.T_Customer_SelectByID 
 ( 
      @ID varchar(50)
 ) 
 AS 
 Select * from dbo.T_Customer Where ID= @ID 
 go 

4)提供查询处理,并返回多条记录集合;包括实体列表集合或DataTable集合对象
对于返回多条集合的对象,在存储过程里面体现都一样的,我们可能在C#处理的时候,把它转换为不同的对象即可,返回多个集合,在SQLServer里面,它们的存储过程代码如下所示。

------------------------------------
--作者:伍华聪 http://wuhuacong.cnblogs.com
--创建时间:2014年11月27日 
--功能描述:检索表中所有的数据 
------------------------------------
 CREATE PROCEDURE dbo.T_Customer_SelectAll 
 AS 
 Select * from dbo.T_Customer 
 go 

2、Oracle存储过程的编写

对应客户表T_Customer,Oracle的创建脚本如下所示。

CREATE TABLE T_CUSTOMER ( 
    ID        VARCHAR2(100),
    NAME    VARCHAR2(50)     NOT NULL ,
    AGE        INTEGER         NOT NULL,
    CREATOR    VARCHAR2(50)    NULL,
    CREATETIME    DATE         DEFAULT SYSDATE,
);

ALTER TABLE T_CUSTOMER ADD CONSTRAINT PK_T_CUSTOMER PRIMARY KEY (ID);

对应SQLServer的存储过程,Oracle的存储过程也提供了对应的版本,下面是几种情况下的Oracle存储过程的编写。
1)提供执行处理,可对执行结果进行反馈

------------------------------------
--作者:伍华聪 http://wuhuacong.cnblogs.com
--创建时间:2014年11月27日 
--功能描述:插入数据到表中 
------------------------------------
 Create Or Replace Procedure T_Customer_Insert 
 ( 
     p_ID IN T_CUSTOMER.ID%TYPE,
     p_Name IN T_CUSTOMER.NAME%TYPE,
     p_Age IN T_CUSTOMER.AGE%TYPE
 ) 
 AS 
 Begin 
 Insert into T_CUSTOMER( ID,NAME,AGE ) Values( p_ID,p_Name,p_Age ) ;
 Commit; 
 Exception 
     When Others Then 
 Rollback; 

 End; 
 / 
------------------------------------
--作者:伍华聪 http://wuhuacong.cnblogs.com
--创建时间:2014年11月27日 
--功能描述:以字段ID为关键字,修改表中的数据 
------------------------------------
 Create Or Replace Procedure T_Customer_UpdateByID 
 ( 
     p_ID IN T_CUSTOMER.ID%TYPE,
     p_Name IN T_CUSTOMER.NAME%TYPE,
     p_Age IN T_CUSTOMER.AGE%TYPE
 ) 
 AS 
 Begin 
 Update T_CUSTOMER Set NAME=p_Name,AGE=p_Age Where ID= p_ID ;
 Commit; 
 Exception 
     When Others Then 
 Rollback; 

 End; 
 / 
------------------------------------
--作者:伍华聪 http://wuhuacong.cnblogs.com
--创建时间:2014年11月27日 
--功能描述:以字段ID为关键字,删除表的记录 
------------------------------------
 Create Or Replace Procedure T_Customer_DeleteByID 
 ( 
      p_ID IN T_CUSTOMER.ID%TYPE 
 ) 
 AS 
 Begin 
 Delete From T_CUSTOMER where ID=p_ID ;
 Commit; 
 Exception 
     When Others Then 
 Rollback; 

 End; 
 / 

其中上面的代码涉及几个地方,T_CUSTOMER.ID%TYPE是表示根据字段动态决定参数的类型,避免应硬编码或者反复修改参数类型。

Oracle的参数一般使用p_的前缀开始,方便区分。

2)提供执行处理,获得一个或者多个返回性参数,并可对执行结果进行反馈。

------------------------------------
--作者:伍华聪 http://wuhuacong.cnblogs.com
--创建时间:2014年11月27日 
--功能描述:以字段ID为关键字,检查表中是否存在符合条件的记录 
------------------------------------
 Create Or Replace Procedure T_Customer_ExistByID 
 ( 
     p_Exist OUT Number  ,
     p_ID IN T_CUSTOMER.ID%TYPE 
 ) 
 AS 
 Begin 
 --V9.i以下使用的语句 
 Select Case When (Count(1)>0) Then 1 Else 0 End Into p_Exist From T_CUSTOMER Where ID=p_ID ;
 --也可以使用的语句 
 -- Select Decode(Count(1),0,0,1) Into p_Exist From T_CUSTOMER Where ID=p_ID ;
 End; 
 / 

------------------------------------
--作者:伍华聪 http://wuhuacong.cnblogs.com
--创建时间:2014年11月27日 
--功能描述:获取表用来标识字段的最大ID值,在标识ID非自增字段时可用于数据插入时调用 
------------------------------------
 Create Or Replace Procedure T_Customer_MaxAge 
 ( 
     p_MaxAge OUT Number 
 ) 
 AS 
 Begin 
 Select Decode(Max(Age) ,NULL,0,Max(Age)) Into p_MaxAge From T_CUSTOMER; 
 End; 
 / 

上面的代码,都有一个输出的参数,虽然他们执行没有影响记录函数,但是这个主要是通过输出参数的值进行处理了。

3)提供查询处理,并返回实体对象

提供查询处理,不管返回一条记录,还是多条记录,在Oracle里面,一般都是通过游标进行处理的,因此我们需要先定义一个游标类型,供我们返回记录使用的。

下面定义一个游标的包代码如下。

------------------------------------
--作者:伍华聪 http://wuhuacong.cnblogs.com
--创建时间:2014年11月27日 
--功能描述:创建一个包,含有一个游标类型:(一个数据库中只需声明一次)  
------------------------------------
 CREATE OR REPLACE PACKAGE MyCURSOR 
 AS 
     TYPE cur_OUT IS REF CURSOR; 
 End; 
 / 

然后我们就可以在各个返回记录的存储过程里面使用这个游标类型了。

例如在下面的存储过程里面,返回一条指定的数据记录,那么输出参数里面需要有一个游标的定义参数,但是我们在C#里面使用数据访问框架来处理数据的时候,可以忽略他它的存在,就只需要输入p_ID参数就可以了。

------------------------------------
--作者:伍华聪 http://wuhuacong.cnblogs.com
--创建时间:2014年11月27日 
--功能描述:以字段ID为关键字,检索表中的数据 
------------------------------------
 Create Or Replace Procedure T_Customer_SelectByID 
 ( 
     cur_OUT OUT MyCURSOR.cur_OUT  ,
     p_ID IN T_CUSTOMER.ID%TYPE 
 ) 
 AS 
 Begin 
 OPEN cur_OUT FOR Select * from T_CUSTOMER Where ID = p_ID ; 
 End; 
 / 

4)提供查询处理,并返回多条记录集合;包括实体列表集合或DataTable集合对象

和上面返回单条记录一样,需要返回多条记录的存储过程,也需要使用一个游标的输出参数来获取返回的记录,并可以对游标进行处理。

------------------------------------
--作者:伍华聪 http://wuhuacong.cnblogs.com
--创建时间:2014年11月27日 
--功能描述:检索表中所有的数据 
------------------------------------
 Create Or Replace Procedure T_Customer_SelectAll 
 ( cur_OUT OUT MyCURSOR.cur_OUT ) 
 AS 
 Begin 
 OPEN cur_OUT FOR Select * from T_CUSTOMER; 
 End; 
 / 

最后,我们看看SQLServer和Oracle数据库的脚本完整情况。

SQLServer存储过程代码:

------------------------------------
--作者:伍华聪 http://wuhuacong.cnblogs.com
--创建时间:2014年11月27日 
--功能描述:插入数据到表中 
------------------------------------
 CREATE PROCEDURE dbo.T_Customer_Insert 
 ( 
      @ID varchar(50),
      @Name varchar(50) ,
      @Age int 
 ) 
 AS 
 begin tran 
 Insert into dbo.T_Customer( ID,Name,Age ) Values( @ID,@Name,@Age ) 
 if @@error!=0 
     begin 
         rollback 
     end 
 else 
     begin 
         commit 
     end 
 go 
 
------------------------------------
--作者:伍华聪 http://wuhuacong.cnblogs.com
--创建时间:2014年11月27日 
--功能描述:以字段ID为关键字,修改表中的数据 
------------------------------------
 CREATE PROCEDURE dbo.T_Customer_UpdateByID 
 ( 
      @ID varchar(50),
      @Name varchar(50) ,
      @Age int 
 ) 
 AS 
 begin tran 
 Update dbo.T_Customer Set Name=@Name,Age=@Age Where ID= @ID 
 if @@error!=0 
     begin 
         rollback 
     end 
 else 
     begin 
         commit 
     end 
 go 
 
------------------------------------
--作者:伍华聪 http://wuhuacong.cnblogs.com
--创建时间:2014年11月27日 
--功能描述:检索表中所有的数据 
------------------------------------
 CREATE PROCEDURE dbo.T_Customer_SelectAll 
 AS 
 Select * from dbo.T_Customer 
 go 
 
------------------------------------
--作者:伍华聪 http://wuhuacong.cnblogs.com
--创建时间:2014年11月27日 
--功能描述:以字段ID为关键字,检索表中的数据 
------------------------------------
 CREATE PROCEDURE dbo.T_Customer_SelectByID 
 ( 
      @ID varchar(50)
 ) 
 AS 
 Select * from dbo.T_Customer Where ID= @ID 
 go 
 
------------------------------------
--作者:伍华聪 http://wuhuacong.cnblogs.com
--创建时间:2014年11月27日 
--功能描述:以字段ID为关键字,检查表中是否存在符合条件的记录 
------------------------------------
 CREATE PROCEDURE dbo.T_Customer_ExistByID 
 ( 
     @Exist int output , 
      @ID varchar(50)
 ) 
 AS 
 Select @Exist = Case When Exists (Select 1 From dbo.T_Customer Where ID=@ID) Then 1 Else 0 End 
 go 
 
------------------------------------
--作者:伍华聪 http://wuhuacong.cnblogs.com
--创建时间:2014年11月27日 
--功能描述:以字段ID为关键字,删除表的记录 
------------------------------------
 CREATE PROCEDURE dbo.T_Customer_DeleteByID 
 ( 
      @ID varchar(50)
 ) 
 AS 
 begin tran 
 Delete From dbo.T_Customer where ID=@ID 
 if @@error!=0 
     begin 
         rollback 
     end 
 else 
     begin 
         commit 
     end 
 go 
 
------------------------------------
--作者:伍华聪 http://wuhuacong.cnblogs.com
--创建时间:2014年11月27日 
--功能描述:获取客户最大年龄
------------------------------------
 CREATE PROCEDURE dbo.T_Customer_MaxAge
( @MaxAge int output )
 AS 
 Select @MaxAge=Case When Max(Age) is NULL Then 0 Else Max(Age) End  From dbo.T_Customer 
 go 

Oracle存储过程代码:

------------------------------------
--作者:伍华聪 http://wuhuacong.cnblogs.com
--创建时间:2014年11月27日 
--功能描述:插入数据到表中 
------------------------------------
 Create Or Replace Procedure T_Customer_Insert 
 ( 
     p_ID IN T_CUSTOMER.ID%TYPE,
     p_Name IN T_CUSTOMER.NAME%TYPE,
     p_Age IN T_CUSTOMER.AGE%TYPE
 ) 
 AS 
 Begin 
 Insert into T_CUSTOMER( ID,NAME,AGE ) Values( p_ID,p_Name,p_Age ) ;
 Commit; 
 Exception 
     When Others Then 
 Rollback; 

 End; 
 / 
------------------------------------
--作者:伍华聪 http://wuhuacong.cnblogs.com
--创建时间:2014年11月27日 
--功能描述:以字段ID为关键字,修改表中的数据 
------------------------------------
 Create Or Replace Procedure T_Customer_UpdateByID 
 ( 
     p_ID IN T_CUSTOMER.ID%TYPE,
     p_Name IN T_CUSTOMER.NAME%TYPE,
     p_Age IN T_CUSTOMER.AGE%TYPE
 ) 
 AS 
 Begin 
 Update T_CUSTOMER Set NAME=p_Name,AGE=p_Age Where ID= p_ID ;
 Commit; 
 Exception 
     When Others Then 
 Rollback; 

 End; 
 / 
------------------------------------
--作者:伍华聪 http://wuhuacong.cnblogs.com
--创建时间:2014年11月27日 
--功能描述:创建一个包,含有一个游标类型:(一个数据库中只需声明一次)  
------------------------------------
 CREATE OR REPLACE PACKAGE MyCURSOR 
 AS 
     TYPE cur_OUT IS REF CURSOR; 
 End; 
 / 
------------------------------------
--作者:伍华聪 http://wuhuacong.cnblogs.com
--创建时间:2014年11月27日 
--功能描述:检索表中所有的数据 
------------------------------------
 Create Or Replace Procedure T_Customer_SelectAll 
 ( cur_OUT OUT MyCURSOR.cur_OUT ) 
 AS 
 Begin 
 OPEN cur_OUT FOR Select * from T_CUSTOMER; 
 End; 
 / 
------------------------------------
--作者:伍华聪 http://wuhuacong.cnblogs.com
--创建时间:2014年11月27日 
--功能描述:以字段ID为关键字,检索表中的数据 
------------------------------------
 Create Or Replace Procedure T_Customer_SelectByID 
 ( 
     cur_OUT OUT MyCURSOR.cur_OUT  ,
     p_ID IN T_CUSTOMER.ID%TYPE 
 ) 
 AS 
 Begin 
 OPEN cur_OUT FOR Select * from T_CUSTOMER Where ID = p_ID ; 
 End; 
 / 
------------------------------------
--作者:伍华聪 http://wuhuacong.cnblogs.com
--创建时间:2014年11月27日 
--功能描述:以字段ID为关键字,检查表中是否存在符合条件的记录 
------------------------------------
 Create Or Replace Procedure T_Customer_ExistByID 
 ( 
     p_Exist OUT Number  ,
     p_ID IN T_CUSTOMER.ID%TYPE 
 ) 
 AS 
 Begin 
 --V9.i以下使用的语句 
 Select Case When (Count(1)>0) Then 1 Else 0 End Into p_Exist From T_CUSTOMER Where ID=p_ID ;
 --V8.i及以下使用的语句 
 -- Select Decode(Count(1),0,0,1) Into p_Exist From T_CUSTOMER Where ID=p_ID ;
 End; 
 / 
------------------------------------
--作者:伍华聪 http://wuhuacong.cnblogs.com
--创建时间:2014年11月27日 
--功能描述:以字段ID为关键字,删除表的记录 
------------------------------------
 Create Or Replace Procedure T_Customer_DeleteByID 
 ( 
      p_ID IN T_CUSTOMER.ID%TYPE 
 ) 
 AS 
 Begin 
 Delete From T_CUSTOMER where ID=p_ID ;
 Commit; 
 Exception 
     When Others Then 
 Rollback; 

 End; 
 / 
------------------------------------
--作者:伍华聪 http://wuhuacong.cnblogs.com
--创建时间:2014年11月27日 
--功能描述:获取表用来标识字段的最大ID值,在标识ID非自增字段时可用于数据插入时调用 
------------------------------------
 Create Or Replace Procedure T_Customer_MaxAge 
 ( 
     p_MaxAge OUT Number 
 ) 
 AS 
 Begin 
 Select Decode(Max(Age) ,NULL,0,Max(Age)) Into p_MaxAge From T_CUSTOMER; 
 End; 
 / 

以上就是存储过程编写过程中的处理和对比,下一篇将继续介绍这个主体,并针对性的介绍如何在C#底层数据访问里面,对这些存储过程的使用。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容