Flutter第十三章(sqflite 数据库,数据库的 CRUD操作,数据库的事务和批处理)

版权声明:本文为作者原创书籍。转载请注明作者和出处,未经授权,严禁私自转载,侵权必究!!!

情感语录: 生活本来就是一场恶战,给止疼药也好,给巴掌也罢,最终都是要单枪匹马练就自身胆量,谁也不例外。

欢迎来到本章节,上一章节介绍了用shared_preferences和文件操作实现持久化,知识点回顾 戳这里 Flutter基础第十二章

承继上一篇文章的伏笔,本章知识点主要介绍 Flutter 的数据持久化之数据库。很多时候我们的数据并不是单一结构且存在关系性,并需对大批量数据有增、删、改、查 操作时那么对数据库的使用那就是必不可少了。

本章简要:

1、sqflite 数据库

2、数据库的 CRUD操作

3、数据库的事务和批处理

一、sqflite 数据库

sqflite数据库是一款轻量级的关系型数据库,如同 iOS和Android中的SQLite。sqflite地址:https://github.com/tekartik/sqflite

sqflite 插件引入

1、在pubspec.yaml文件中添加依赖
    dependencies:
      fluttertoast: ^3.0.3
      flutter:
        sdk: flutter
      #添加持久化插件 sp
      shared_preferences: ^0.5.3+1
      #添加文件库
      path_provider: ^1.2.0
      #添加数据库
      sqflite: ^1.1.6

本人使用的当前最新版本 1.1.6,读者想体验最新版本请在使用时参看最新版本号进行替换。

2、安装依赖库

执行 flutter packages get 命令;AS 开发工具直接右上角 packages get也可。

3、在需要使用的地方导包引入
import 'package:sqflite/sqflite.dart';

sqflite 支持的数据类型

    存储类             描述
     
    NULL              值是一个 NULL 值。
    
    INTEGER           值是一个带符号的整数,-2^63 到 2^63 - 1
    
    REAL              值是一个数字类型,dart中的 num
    
    TEXT              值是一个文本字符串,dart中的 String
    
    BLOB              值是一个 blob 数据,dart中的 Uint8List 或者 List<int> 

可以看出 sqflite 中支持的数据类型比较少,比如 bool 、DateTime都是不支持的;开发中需要 bool 类型可以使用 INTEGER的 0和1来表示,DateTime 类型可以使用 时间戳 字符串。

sqflite 中常用的 API:

getDatabasesPath() : 获取数据库位置,在Android上,它通常是data/data/包名/databases;在iOS上,它是Documents目录。

join("参数1", "参数2"): 该方法表示创建数据库, 参数1: getDatabasesPath() 获取到的数据库存放路径,参数2: 数据库的名字,如:User.db

openDatabase(): 该方法表示打开数据,具体有以下几个重要参数

Future<Database> openDatabase(
    String path,  
    {int version,
    OnDatabaseConfigureFn onConfigure, 
    OnDatabaseCreateFn onCreate,
    OnDatabaseVersionChangeFn onUpgrade,
    OnDatabaseVersionChangeFn onDowngrade,
    OnDatabaseOpenFn onOpen,
    bool readOnly = false,
    bool singleInstance = true})

path:必传参数,join() 创建数据库后的返回值。

version: 当前的版本号。

onConfigure: 数据库的相关配置

onCreate: 创建表的方法

onUpgrade、onDowngrade: 数据库版本的升降级

readOnly: 是否为只读方式打开。

CRUD 相关API:

1、插入数据的两种方式:
Future<int> insert(String table, Map<String, dynamic> values,
      {String nullColumnHack, ConflictAlgorithm conflictAlgorithm});

Future<int> rawInsert(String sql, [List<dynamic> arguments]);

insert() 方法第一个参数为操作的表名,第二个参数 map 中是想要添加的字段名和对应字段值。

rawInsert() 方法第一个参数为一条插入sql 语句,语句中?作为占位符,通过第二个参数填充占位数据。

2、查询数据的两种方式:
Future<List<Map<String, dynamic>>> query(String table,
      {bool distinct,
      List<String> columns,
      String where,
      List<dynamic> whereArgs,
      String groupBy,
      String having,
      String orderBy,
      int limit,
      int offset});
      
Future<List<Map<String, dynamic>>> rawQuery(String sql,
      [List<dynamic> arguments]);
2.1 query() 方式查询的参数介绍:
    参数           描述

    table          表名

    distinct       是否去重

    columns        查询字段集合

    where          WHERE子句(使用?作为占位符)

    whereArgs      WHERE子句占位符参数值

    groupBy        结果集分组

    having         结合groupBy使用过滤结果集

    orderBy        排序方式

    limit          查询的条数

    offset         查询的偏移位
2.2 rawQuery() 方法第一个参数为一条查询sql语句,使用 ?作为占位符,通过第二个参数填充数据。
3. 修改数据的两种方式
Future<int> update(String table, Map<String, dynamic> values,
      {String where,
      List<dynamic> whereArgs,
      ConflictAlgorithm conflictAlgorithm});
      
Future<int> rawUpdate(String sql, [List<dynamic> arguments]);

update()方法第一个参数为操作的表名,第二个参数为修改的字段和对应值,后边的可选参数依次表示WHERE子句、WHERE子句占位符参数值、发生冲突时的操作算法(包括回滚、终止、忽略等)。

rawUpdate() 方法第一个参数为一条更新sql语句,使用?作为占位符,通过第二个参数填充数据。

4. 删除数据的两种方式
Future<int> delete(String table, {String where, List<dynamic> whereArgs});

Future<int> rawDelete(String sql, [List<dynamic> arguments]);

delete() 方法第一个参数为操作的表名,后边的可选参数依次表示WHERE子句、WHERE子句占位符参数值。

rawDelete() 方法第一个参数为一条删除sql语句,使用?作为占位符,通过第二个参数填充数据。

close() 关闭数据库。

transaction() 开启事务。

batch() 获取批处理对象。

可以看到 Flutter 在 增、删、改、查 中都提供了两套方法使用,更倾向于写 sql 语句的客官们使用 rawxxx 方式就比较好;但我还是喜欢通过参数拼接组合的方式,可以屏蔽很多细节问题。

二、数据库的 CRUD

上面对 sqflite 引入和相关 API 都做了介绍,如果还是不清楚怎么使用,接下来就通过 案例的方式去学习。为了不重复的叙述,这里需要提前先做两个准备工作:① 新建一个用户实体类(方便数据操作)。 ② 建库、建表。

用户实体类

    class UserInfo{

      String name;

      String password;

      int age;


      UserInfo({this.name,this.age,this.password});


      UserInfo.toUser(Map<String, dynamic> json) {
        name = json['name'];
        age = json['age'];
        password = json['password'];
      }


      Map<String, dynamic> toMap() {
        Map<String, dynamic> data = new Map<String, dynamic>();
        data['name'] = this.name;
        data['age'] = this.age;
        data['password'] = this.password;
        return data;
      }

    }

建库、建表

    import 'package:sqflite/sqflite.dart';
    import 'package:path/path.dart';

    import 'UserInfo.dart';

    class SqlUserHelper{

      //数据库
      final String dataBaseName = "User.db";

      //数据表
      final String tableName = "USER_TABLE";

      //以下是表中的列名
      final String columnId = 'id';
      final String name = 'name';
      final String password = 'password';
      final String age ="age";

      // 静态私有成员
      static SqlUserHelper _instance;

      Database _database;
      // 私有构造函数
      SqlUserHelper._() {

        initDb();
      }

      //私有访问点
      static SqlUserHelper helperInstance() {
        if (_instance == null) {
          _instance = SqlUserHelper._();
        }
        return _instance;
      }


      //初始化数据库
       void initDb() async {

        String databasesPath = await getDatabasesPath();
        String path = join(databasesPath, dataBaseName );

        // openDatabase 指定是数据库路径,版本号,和执行表的创建
        _database = await openDatabase(path, version: 1, onCreate: _onCreate);
      }

      //创建UserInfo表
      void _onCreate(Database db, int newVersion) async {

        await db.execute('CREATE TABLE $tableName($columnId INTEGER PRIMARY KEY AUTOINCREMENT, $name TEXT, $password TEXT, $age INTEGER)');
      }

      ///关闭数据库
      Future<void> close() async {
        return _database.close();
      }

    }

UserInfo 实体比较简单只声明了三个属性而已,另外两个方法只是为了插入数据和查询时方便观看而已。SqlUserHelper 是一个单例模式的用户数据库操作类( 单例模式不清楚的请看上一章节)。在使用完数据库时一定要及时关闭数据库,避免造成不必要的资源浪费。不持续叨逼叨.... 代码中已有详细注释。

1、添加数据

首先向数据库插入几条数据,方便后面查询使用。在 SqlUserHelper 中添加插入数据的方法。

插入数据的两种方式
      /// insert第一种
      Future<int> insert(UserInfo userInfo){
        return _database.insert(tableName, userInfo.toMap());
      }

      /// insert第二种
      Future<int> rawInsert(UserInfo userInfo){
        return _database.rawInsert("INSERT INTO $tableName ($name,$password,$age) VALUES(?, ?, ?)", [userInfo.name,userInfo.password,userInfo.age]);
      }

实例代码:

    //构建一个user 对象
     UserInfo user = UserInfo(name: userName,password: userPass,age: age);

    //向数据库插入该条数据
     sqlUserHelper.insert(user).then((value){
      print("the last insert id $value");
    });
    
    //构建一个user 对象
    UserInfo user = UserInfo(name: userName,password: userPass,age: age);

    //向数据库插入该条数据
    sqlUserHelper.rawInsert(user).then((value){
      print("the last rawInsert id $value");
    });

无论是通过 insert 还是 rawInsert方式插入数据,只要成功插入就会返回最后一条插入的记录 ID 回来。

实操演示:

insert.gif

控制台输出:

    I/flutter: the last insert id 1
    I/flutter: the last rawInsert id 2

控制台打印出了通过两种方式插入数据后的记录 ID,证明此时已经成功插入了两条一样的数据到数据库。

2、查询数据
查询数据的两种方式
      ///第一种 query
      Future<List<Map>> query() async {
        List<Map> maps = await _database.query(tableName);
        if (maps.isNotEmpty) {
          return maps;
        }
        return null;
      }

      ///第二种 query
      Future<List<Map>> rawQuery() async {
        List<Map> maps = await _database.rawQuery("SELECT * FROM $tableName");
        if (maps.isNotEmpty) {
          return maps;
        }
        return null;
      }

实例代码:

  ///查询全部
   sqlUserHelper.query().then((value){
    print("the query info  ${value.toString()}");
  });
  
  
   ///查询全部
  sqlUserHelper.rawQuery().then((value){
    print("the rawQuery info  ${value.toString()}");
  });

两种方式都是查询整个 user表中的全部信息,并将结果打印输出。下面来查询上面添加的数据。

实操演示:

query.gif

可以看到在我添加数据后,分别点了 query 和 rawQuery 方式查询,控制台输出结果如下:

I/flutter: the query info  [{id: 1, name: zhengzaihong, password: 123456, age: 18}, {id: 2, name: zhengzaihong, password: 123456, age: 18}]

I/flutter: the rawQuery info  [{id: 1, name: zhengzaihong, password: 123456, age: 18}, {id: 2, name: zhengzaihong, password: 123456, age: 18}]

两种方式都查询出了 全部的信息,实际开发中一般都是条件查询,这里只是简单除暴的演示而已。

3、修改数据

上面有了两条一样的数据,接下来方便我们做修改,然后再次查询输出结果看是否正确修改。

修改数据的两种方式
      ///第一种 update
      Future<int> update(UserInfo user,int id) async {
        return await _database.update(tableName,user.toMap(),where: '$columnId = ?', whereArgs: [id]);
      }

      ///第二种 rawUpdate
      Future<int> rawUpdate(UserInfo user,int id) async {
        return await _database.rawUpdate("UPDATE $tableName SET  $name = ?  WHERE $columnId = ? ",[user.name,id]);
      }

实例代码:

      //构建一个user 对象 根据 ID 修改
      UserInfo user = UserInfo(name: userName,password: userPass,age: age);
      sqlUserHelper.update(user, 1).then((value){
        print("the update info  ${value.toString()}");
      });
      
      UserInfo user = UserInfo(name: userName,password: userPass,age: age);
      sqlUserHelper.rawUpdate(user, 2).then((value){
        print("the rawUpdate info  ${value.toString()}");
      });

实操演示

update.gif

控制台输出:

I/flutter: the update info  1

I/flutter: the rawUpdate info  1

I/flutter: the query info  [{id: 1, name: zzh, password: 123456, age: 20}, {id: 2, name: LQ, password: 123456, age: 18}]

I/flutter: the rawQuery info  [{id: 1, name: zzh, password: 123456, age: 20}, {id: 2, name: LQ, password: 123456, age: 18}]

从输出可以看出 无论是 update 还是 rawUpdate方式修改数据都打印出有一行受影响,这表示数据被成功修改了。update 方式把输入框中的数据重新带入把 id =1 的这条数据全部重新赋值一遍,实现了数据修改;而 rawUpdate 方式只在 sql 语句中加入了对名字的修改,可以看到在输入框填写了年龄值也并未修改成功。

4、删除数据
删除数据的两种方式
      ///第一种 delete 根据id删除
      Future<int> delete(int id) async {
        return await _database.delete(tableName,
            where: "$columnId = ?", whereArgs: [id]);
      }


      ///第二种 delete  根据id删除
      Future<int> rawDelete(int id) async {
        return await _database.rawDelete("DELETE FROM $tableName WHERE $columnId = ?", [id]);
      }

实例代码:

  /// 根据 id 删除
  sqlUserHelper.delete(1).then((value){
    print("the delete info  ${value.toString()}");
  });
  
  /// 根据 id 删除
  sqlUserHelper.rawDelete( 2).then((value){
    print("the rawDelete info  ${value.toString()}");
  });

实操演示:

delete.gif

在我点击两次删除按钮后,再次查询结果如下:

    I/flutter: the delete info  1

    I/flutter: the rawDelete info  1

    I/flutter: the query info  null

查询结果输出为 null 表示数据库中已经没有开始存入的两条数据了,证明两次删除都是成功的。

三、数据库的事务和批处理

1、事务

sqflite同时支持事务,所谓事务,它是一个操作序列,这些操作要么都执行,要么都不执行,即:执行单个逻辑功能的一组指令或操作称为事务。

下面通过事务来添加两条数据:

  /// 开启事务添加
  Future<bool> transactionInsert(UserInfo userInfo1, UserInfo userInfo2) async {
    return await _database.transaction((Transaction transaction) async {

      int id1 = await transaction.insert(tableName, userInfo1.toMap());

      int id2 = await transaction.insert(tableName, userInfo2.toMap());

      return id1 != null && id2 != null;
    });
  }

实例代码:

  //构建两个用户对象
  
  UserInfo user1 = UserInfo(name: "zhengxian",password: "123456",age: 18);
  
  UserInfo user2 = UserInfo(name: "zzh",password: "123456",age: 20);
  
  sqlUserHelper.transactionInsert(user1, user2).then((value){
      print("transaction result: $value");
  });

这里就不贴图了,下面直接来看通过事务来添加的两条数据和查询结果,控制台输出:

I/flutter: transaction result: true
I/flutter: the query info  [{id: 1, name: zhengxian, password: 123456, age: 18}, {id: 2, name: zzh, password: 123456, age: 20}]

结果返回 ture ,查询结果也是上面构建的两条数据信息,说明都是成功的。

2、批处理

sqflite支持批处理操作,批处理指的是一次操作中执行多条SQL语句,批处理相比于一次一次执行效率会提高很多。

下面通过批处理来新增一条数据和修改一条数据

    /// 批处理
    Future<List<dynamic>> batch(UserInfo user,UserInfo user2) async {

     Batch batch =  _database.batch();
     //先添加一条数据
     batch.insert(tableName, user.toMap());
     //修改 id 为1的值
     batch.update(tableName,user2.toMap(),where: '$columnId = ?', whereArgs: [1]);

     return batch.commit();
    }

实例代码:

  UserInfo user1 = UserInfo(name: "zhengxian",password: "123456",age: 18);

  UserInfo user2 = UserInfo(name: "LiShi",password: "123456",age: 55);

  sqlUserHelper.batch( user1,user2).then((value){
    print("the batch info  ${value.toString()}");
  });

控制台打印批处理和查询结果:

    I/flutter: the batch info  [3, 1]
    I/flutter: the query info  [{id: 1, name: LiShi, password: 123456, age: 55}, {id: 2, name: zzh, password: 123456, age: 20}, 
          {id: 3, name: zhengxian, password: 123456, age: 18}]

[3, 1] 两个数字分别表示最后一次插入数据后的记录 id,和有一行修改受影响。通过查询我可以看出,数据确实成功插入到了数据库,且 id=1 的这条数据被成功修改了。

下面贴出下数据库操作的源码,界面源码就不贴了,代码全部详情会在文末给出:

  import 'package:sqflite/sqflite.dart';
  import 'package:path/path.dart';
  
  import 'UserInfo.dart';
  
  class SqlUserHelper{
  
    //数据库
    final String dataBaseName = "User.db";
  
    //数据表
    final String tableName = "USER_TABLE";
  
    //以下是表中的列名
    final String columnId = 'id';
    final String name = 'name';
    final String password = 'password';
    final String age ="age";
  
    // 静态私有成员
    static SqlUserHelper _instance;
  
    Database _database;
    // 私有构造函数
    SqlUserHelper._() {
  
      initDb();
    }
  
    //私有访问点
    static SqlUserHelper helperInstance() {
      if (_instance == null) {
        _instance = SqlUserHelper._();
      }
      return _instance;
    }
  
  
    //初始化数据库
     void initDb() async {
  
      String databasesPath = await getDatabasesPath();
      String path = join(databasesPath, dataBaseName );
  
      // openDatabase 指定是数据库路径,版本号,和执行表的创建
      _database = await openDatabase(path, version: 1, onCreate: _onCreate);
    }
  
    //创建UserInfo表
    void _onCreate(Database db, int newVersion) async {
  
      await db.execute('CREATE TABLE $tableName($columnId INTEGER PRIMARY KEY AUTOINCREMENT, $name TEXT, $password TEXT, $age INTEGER)');
    }
  
  
    /// 插入数据的两种方式
    ///
    /// insert第一种
    Future<int> insert(UserInfo userInfo){
      print("插入数据:${ userInfo.toMap()}");
      return _database.insert(tableName, userInfo.toMap());
    }
  
    /// insert第二种
    Future<int> rawInsert(UserInfo userInfo){
      return _database.rawInsert("INSERT INTO $tableName ($name,$password,$age) VALUES(?, ?, ?)", [userInfo.name,userInfo.password,userInfo.age]);
    }
  
  
    ///查询数据的两种方式
  
    ///第一种 query
    Future<List<Map>> query() async {
      List<Map> maps = await _database.query(tableName);
      if (maps.isNotEmpty) {
        return maps;
      }
      return null;
    }
  
    ///第二种 query
    Future<List<Map>> rawQuery() async {
      List<Map> maps = await _database.rawQuery("SELECT * FROM $tableName");
      if (maps.isNotEmpty) {
        return maps;
      }
      return null;
    }
  
  
  
    ///修改数据的两种方式
  
    ///第一种 update
    Future<int> update(UserInfo user,int id) async {
      return await _database.update(tableName,user.toMap(),where: '$columnId = ?', whereArgs: [id]);
    }
  
    ///第二种 rawUpdate
    Future<int> rawUpdate(UserInfo user,int id) async {
      return await _database.rawUpdate("UPDATE $tableName SET  $name = ?  WHERE $columnId = ? ",[user.name,id]);
    }
  
  
    ///删除数据的两种方式
    ///
    ///第一种 delete 根据id删除
    Future<int> delete(int id) async {
      return await _database.delete(tableName,
          where: "$columnId = ?", whereArgs: [id]);
    }
  
  
    ///第二种 delete  根据id删除
    Future<int> rawDelete(int id) async {
      return await _database.rawDelete("DELETE FROM $tableName WHERE $columnId = ?", [id]);
    }
  
  
    /// 开启事务添加
    Future<bool> transactionInsert(UserInfo userInfo1, UserInfo userInfo2) async {
      return await _database.transaction((Transaction transaction) async {
  
        int id1 = await transaction.insert(tableName, userInfo1.toMap());
  
        int id2 = await transaction.insert(tableName, userInfo2.toMap());
  
        return id1 != null && id2 != null;
      });
    }
  
  
     /// 批处理
     Future<List<dynamic>> batch(UserInfo user,UserInfo user2) async {
  
       Batch batch =  _database.batch();
       //先添加一条数据
       batch.insert(tableName, user.toMap());
       //修改 id 为1的值
       batch.update(tableName,user2.toMap(),where: '$columnId = ?', whereArgs: [1]);
  
       return batch.commit();
    }
  
  
    ///关闭数据库
    Future<void> close() async {
      return _database.close();
    }
  
  }

至此数据库的一些用法也就介绍完了,加上前一篇文章中介绍的 ShardPreferences 和 IO 文件读写方式总共介绍了三种方式可以实现数据持久化。ShardPreferences多用于一些简单无关系性的数据存储;IO 文件的读写其实在开发中很少使用来存储数据,往往都是做一些文档文件才会使用。

好了本章节到此结束,又到了说再见的时候了,如果你喜欢请留下你的小红星,创作真心也不容易;你们的支持才是创作的动力,如有错误,请热心的你留言指正, 谢谢大家观看,下章再会 O(∩_∩)O

实例源码地址:https://github.com/zhengzaihong/flutter_learn/tree/master/lib/page/database

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

推荐阅读更多精彩内容