iOS SQLite数据库基础

标签(空格分隔): iOS数据库 数据库总结 SQLite数据库


ios中的数据存储方式及其特点

  1. Preference(偏好设置\NSUserDefaults):也不能存储自定义对象
  2. NSCoding(NSKeyedArchiver\NSkeyedUnarchiver):归档,局限:一次性存取,读全部读出来,写会覆盖
  3. SQLite3 :关系型数据库,不能直接存储对象,要将对象拆开存储
  4. Core Data :对象型的数据库,内部透明

一、什么是SQLite

什么是数据库?
数据库(Database)是按照数据结构来组织、存储和管理数据的仓库
数据库可以分为2大种类:1. 关系型数据库(主流) 2. 对象型数据库

SQLite是一款轻型的嵌入式数据库,它占用资源非常的低,在嵌入式设备中,可能只需要几百K的内存就够了,它的处理速度比Mysql、PostgreSQL这两款著名的数据库都还快。


二、SQL语句

在程序运行过程中操作数据库中的数据,得先学会使用SQL语句

1. 什么是SQL

SQL(structured query language):结构化查询语言
SQL是一种对关系型数据库中的数据进行定义和操作的语言
SQL语言简洁,语法简单,好学好用

2. 什么是SQL语句

使用SQL语言编写出来的句子\代码,就是SQL语句
在程序运行过程中,要想操作(增删改查,CRUD)数据库中的数据,必须使用SQL语句

3. SQL语句的特点

不区分大小写(比如数据库认为user和UsEr是一样的)
每条语句都必须以分号 ; 结尾

4. SQL中的常用关键字

select、insert、update、delete、from、create、where、desc、order、by、group、table、alter、view、index等等

5. SQL语句的种类

  1. 数据定义语句(DDL:Data Definition Language)
    包括create和drop等操作
    在数据库中创建新表或删除表(create table或 drop table)
  2. 数据操作语句(DML:Data Manipulation Language)
    包括insert、update、delete等操作
    上面的3种操作分别用于添加、修改、删除表中的数据
  3. 数据查询语句(DQL:Data Query Language)
    可以用于查询获得表中的数据
    关键字select是DQL(也是所有SQL)用得最多的操作
    其他DQL常用的关键字有where,order by,group by和having

三、数据库操作

表操作

1. 创建表

格式:

create table 表名 (字段名1 字段类型1, 字段名2 字段类型2, …) ;
create table if not exists 表名 (字段名1 字段类型1, 字段名2 字段类型2, …) ;
示例:
create table t_student (id integer, name text, age inetger, score real) ;

字段类型

integer : 整型值
real : 浮点值
text : 文本字符串
blob : 二进制数据(比如文件)

实际上是无类型的
就算声明为integer类型,还是能存储字符串文本(主键除外)
建表时声明啥类型或者不声明类型都可以,也就意味着创表语句可以这么写:
create table t_student(name, age);

为了保持良好的编程规范、方便程序员之间的交流,编写建表语句的时候最好加上每个字段的具体类型

2. 删除表

格式:

drop table 表名 ;
drop table if exists 表名 ;

示例:
drop table t_student ;

数据操作

1. 插入数据

格式:

insert into 表名 (字段1, 字段2, …) values (字段1的值, 字段2的值, …) ;

示例
insert into t_student (name, age) values (‘mj’, 10) ;

//注意!
数据库中的字符串内容应该用一对单引号 ' 包含

2. 更新数据

格式:

update 表名 set 字段1 = 字段1的值, 字段2 = 字段2的值, … ; 

示例
update t_student set name = ‘jack’, age = 20 ; 

//注意!
上面的示例会将t_student表中所有记录的name都改为jack,age都改为20

3. 删除数据

格式:

delete from 表名 ;

示例
delete from t_student ;

注意
上面的示例会将t_student表中所有记录都删掉

4. 条件语句

如果只想删除数据库中国某些指定的记录,那么就必须在DML语句后加上一些条件
条件语句的格式:

where 字段 = 某个值 ;   // 不能用两个 =
where 字段 is 某个值 ;   // is 相当于 = 
where 字段 != 某个值 ; 
where 字段 is not 某个值 ;   // is not 相当于 != 
where 字段 > 某个值 ; 
where 字段1 = 某个值 and 字段2 > 某个值 ;  // and相当于C语言中的 &&
where 字段1 = 某个值 or 字段2 = 某个值 ;  //  or 相当于C语言中的 ||

示例:
将t_student表中年龄大于10 并且 姓名不等于jack的记录,年龄都改为 5
update t_student set age = 5 where age > 10 and name != ‘jack’ ;

删除t_student表中年龄小于等于10 或者 年龄大于30的记录
delete from t_student where age <= 10 or age > 30 ;

猜猜下面语句的作用
update t_student set score = age where name = ‘jack’ ;
将t_student表中名字等于jack的记录,score字段的值 都改为 age字段的值

5. 查询数据

格式:

select 字段1, 字段2, … from 表名 ;
select * from 表名;   //  查询所有的字段

示例:
select name, age from t_student ;
select * from t_student ;
select * from t_student where age > 10 ;  //  条件查询

6. 起别名(了解)

格式:(字段和表都可以起别名)

select 字段1 别名 , 字段2 别名 , … from 表名 别名 ; 
select 字段1 别名, 字段2 as 别名, … from 表名 as 别名 ;
select 别名.字段1, 别名.字段2, … from 表名 别名 ;
示例
select name myname, age myage from t_student ;
给name起个叫做myname的别名,给age起个叫做myage的别名
select s.name, s.age from t_student s ;
给t_student表起个别名叫做s,利用s来引用表中的字段

7. 计算记录的数量

格式:

select count (字段) from 表名 ;
select count ( * ) from 表名 ;

示例
select count (age) from t_student ;
select count ( * ) from t_student where score >= 60;

8. 排序

按照某个字段的值进行排序搜索
select * from t_student order by 字段 ;
Example: select * from t_student order by age ;
//默认是按照升序排序(由小到大),也可以变为降序(由大到小)

//指定升降序
select * from t_student order by age desc ;  //降序
select * from t_student order by age asc ;   // 升序(默认)

//也可以用多个字段进行排序
//如下,先按照年龄排序(升序),年龄相等就按照身高排序(降序)
select * from t_student order by age asc, height desc ;

9. 精确控制查询结果

使用limit可以精确地控制查询结果的数量,比如每次只查询10条数据
格式:

select * from 表名 limit 数值1, 数值2 ;

示例:
select * from t_student limit 4, 8 ;
//可以理解为:跳过最前面4条语句,然后取8条记录

limit常用来做分页查询

比如每页固定显示5条数据,那么应该这样取数据
第1页:limit 0, 5
第2页:limit 5, 5
第3页:limit 10, 5
…
第n页:limit 5*(n-1), 5

//注:!省略了limit的第一个参数
select * from t_student limit 7 ;
相当于select * from t_student limit 0, 7 ;
表示取最前面的7条记录

约束

1. 简单约束

建表时可以给特定的字段设置一些约束条件,常见的约束有

not null :规定字段的值不能为null
unique :规定字段的值必须唯一
default :指定字段的默认值
//建议:尽量给字段设定严格的约束,以保证数据的规范性

示例
create table t_student (id integer, name text not null unique, age integer not null default 1) ;
//name字段不能为null,并且唯一
//age字段不能为null,并且默认为1

2. 主键约束

如果t_student表中就name和age两个字段,而且有些记录的name和age字段的值都一样时,那么就没法区分这些数据,造成数据库的记录不唯一,这样就不方便管理数据

良好的数据库编程规范应该要保证每条记录的唯一性,为此,增加了主键约束。也就是说,每张表都必须有一个主键,用来标识记录的唯一性

1. 什么是主键

主键(Primary Key,简称PK)用来唯一地标识某一条记录
例如:t_student可以增加一个id字段作为主键,相当于人的身份证
主键可以是一个字段或多个字段

2. 主键的特点

主键应当是对用户没有意义的
永远也不要更新主键
主键不应包含动态变化的数据
主键应当由计算机自动生成

4. 主键的声明

在创表的时候用primary key声明一个主键

create table t_student (id integer primary key, name text, age integer) ;
integer类型的id作为t_student表的主键

主键字段
只要声明为primary key,就说明是一个主键字段
主键字段默认就包含了not null 和 unique 两个约束

如果想要让主键自动增长(必须是integer类型),应该增加autoincrement

create table t_student (id integer primary key autoincrement, name text, age integer) ;

3. 外键约束

利用外键约束可以用来建立表与表之间的联系
外键的一般情况是:一张表的某个字段,引用着另一张表的主键字段

1. 新建一个外键

create table t_student (id integer primary key autoincrement, name text, age integer, class_id integer, constraint fk_t_student_class_id_t_class_id foreign key (class_id)  references t_class (id);

t_student表中有一个叫做fk_t_student_class_id_t_class_id的外键
这个外键的作用是用t_student表中的class_id字段引用t_class表的id字段

2. 表连接查询

什么是表连接查询?
需要联合多张表才能查到想要的数据

表连接的类型
内连接:inner join 或者 join (显示的是左右表都有完整字段值的记录)
左外连接:left outer join (保证左表数据的完整性)

示例
查询0316iOS班的所有学生
select s.name,s.age from t_student s, t_class c where s.class_id = c.id and c.name = ‘0316iOS’;

三、iOS基于C语言的数据库操作

1.首先创建并打开数据库

// 打开数据库
- (void)openDatabase:(NSString *)SQLiteName {
    //1. 获取数据库存储路径
    NSString *dbName = [SQLiteName documentDir];
    //2. 打开数据库
    // 如果数据库不存在,怎新建并打开一个数据库,否则直接打开
    if (sqlite3_open(dbName.UTF8String, &_db) != SQLITE_OK) {
        NSLog(@"创建/打开数据库失败。");
    }

    //3. 创建表
    if ([self createTable]) {
        NSLog(@"创建表成功");
    } else {
        NSLog(@"创建表失败");
    }
}

/**
 *  创建数据表
 */
- (BOOL)createTable {
    NSString *sql = @"CREATE TABLE IF NOT EXISTS t_person (id integer PRIMARY KEY AUTOINCREMENT,name text,age integer)";

    return [self execSql:sql];
}

2. 增删改

sqlite3_exec() 方法可以执行任何SQL语句,比如创表、更新、插入和删除操作。但是一般不用它执行查询语句,因为它不会返回查询到的数据。我们这里封装一个方法。

/**
 *  执行除查询以外的sql语句
 */
- (BOOL)execSql:(NSString *)sql {
    if (sqlite3_exec(_db, sql.UTF8String, nil, nil, nil) != SQLITE_OK) {
        return NO;
    }
    return YES;
}

3.查询

因为查询语句需要返回查询结果并提供给UI显示,所以是最麻烦的
查询用到的主要有以下三个方法:

sqlite3_prepare_v2() : 准备数据库,并检查SQL语句是否合法
sqlite3_step() :一条一条获取数据,直到没有记录
sqlite3_coloum_xxx() : 获取对应类型的内容,从0开始。根据实际查询字段的属性,使用sqlite3_column_字段类型 取得对应的内容即可。

查询完要释放资源:

sqlite3_finalize() : 释放stmt

*
 *  返回指定sql查询语句运行的结果集
 *
 *  @param sql sql
 *
 *  @return 结果集
 */
- (NSArray *)execRecordSql:(NSString *)sql {
    // 1. 评估准备SQL语法是否正确
    sqlite3_stmt *stmt = NULL;

    /**
     *  准备: 理解为预编译SQL语句, 检测里面是否有错误等等, 它可以提供性能
     *
     *  @param db   已经开打的数据库对象
     *  @param cSQL 需要执行的SQL语句
     *  @param -1   需要执行的SQL语句的长度, 传入-1系统自动计算
     *  @param stmt 句柄,靠这个获取查询结果,当成缓冲区就好了
     *  @param NULL  一般传NULL
     *
     *  @return
     */
    if (sqlite3_prepare_v2(_db, sql.UTF8String, -1, &stmt, NULL) != SQLITE_OK) {
        NSLog(@"准备数据失败");
    }
    NSMutableArray *records = [NSMutableArray array];
    // 2.查询数据
    // sqlite3_step代表取出一条数据, 如果取到了数据就会返回SQLITE_ROW
    while (sqlite3_step(stmt) == SQLITE_ROW) {
        // 3. 获取/显示查询结果
        // sqlite3_column_xxx方法的第二个参数与sql语句中的字段顺寻一一对应(从0开始)
        // 获取一条记录的数据
        NSDictionary *dict = [self recordWithStmt:stmt];
        [records addObject:dict];
    }

    // 4. 释放句柄
    sqlite3_finalize(stmt);

    // 返回查询到的数据
    return records;
}

/**
 获取一条记录的值

 - parameter stmt: 预编译好的SQL语句

 - returns: 字典
 */
- (NSDictionary *)recordWithStmt:(sqlite3_stmt *)stmt {
    //1.拿到当前这条数据的所有列
    int count = sqlite3_column_count(stmt);
    // 定义字典存储查询到的数据
    NSMutableDictionary *record = [[NSMutableDictionary alloc] init];

    for (int index = 0; index < count; index++) {
        // 2. 拿到每一列的名称
        NSString *name = [NSString stringWithCString:sqlite3_column_name(stmt, index) encoding:NSUTF8StringEncoding];
        NSLog(@"%@",name);
        // 3.拿到每一列的类型 SQLITE_INTEGER
        int type = sqlite3_column_type(stmt, index);

        switch (type) {
            case SQLITE_INTEGER://整形
                [record setObject:@(sqlite3_column_int64(stmt, index)) forKey:name];
                break;
            case SQLITE_FLOAT:
                [record setObject:@(sqlite3_column_double(stmt, index)) forKey:name];
                break;
            case SQLITE3_TEXT:
                // 文本类型
                [record setObject:[NSString stringWithUTF8String:sqlite3_column_text(stmt, index)] forKey:name];
                break;
            case SQLITE_NULL:
                // 空类型
                [record setObject:[[NSNull alloc]init] forKey:name];
                break;
            default:
                // 二进制类型 SQLITE_BLOB
                // 一般情况下, 不会往数据库中存储二进制数据
                break;
        }
    }

    return record;

四、FMDB框架的使用

这一部分来自:张兴业的CSDN

1.简介

FMDB是iOS平台的SQLite数据库框架,它以OC的方式封装了SQLite的Cell语言API。类似的封装库还有PlausibleDatabase、sqlitepersistentobjects等。FMDB更加简单易用并且支持线程安全所以更加流行。

FMDB是一个开源的项目:GitHub连接

2.添加FMDB框架

从GitHub下载完成后解压,导入fmdb文件夹下的11个文件,如下图:

Screen Shot 2016-04-30 at 21.52.18.png

使用FMDB也必须加入 libsqlite3.dylib 依赖包。
FMDB同时兼容ARC和非ARC工程,会自动根据工程配置来调整相关的内存管理代码。
FMDB常用类:

FMDatabase : 一个单一的SQLite数据库,用于执行SQL语句。
FMResultSet :执行查询一个FMDatabase结果集,这个和Android的Cursor类似。
FMDatabaseQueue :在多个线程来执行查询和更新时会使用这个类。

3.创建数据库

通过指定SQLite数据库文件路径来创建FMDatabase对象

FMDatabase *db = [FMDatabase databaseWithPath:path];
if (![db open]) {
NSLog(@"数据库打开失败!");
}

1、当数据库文件不存在时,fmdb会自己创建一个。
2、 如果你传入的参数是空串:@"",则fmdb会在临时文件目录下创建这个数据库,数据库断开连接时,数据库文件被删除。
3、如果你传入的参数是 NULL,则它会建立一个在内存中的数据库,数据库断开连接时,数据库文件被删除

4.打开和关闭数据库

[db open] ;
[db close];

5.增删改

除了查询操作,FMDB数据库操作都执行executeUpdate方法,这个方法返回BOOL型。

创建表

if ([db open]) {  
        NSString *sqlCreateTable =  [NSString stringWithFormat:@"CREATE TABLE IF NOT EXISTS '%@' ('%@' INTEGER PRIMARY KEY AUTOINCREMENT, '%@' TEXT, '%@' INTEGER, '%@' TEXT)",TABLENAME,ID,NAME,AGE,ADDRESS];  
        BOOL res = [db executeUpdate:sqlCreateTable];  
        if (!res) {  
            NSLog(@"error when creating db table");  
        } else {  
            NSLog(@"success to creating db table");  
        }  
        [db close];  
  
    } 

添加数据

if ([db open]) {  
       NSString *insertSql1= [NSString stringWithFormat:  
                              @"INSERT INTO '%@' ('%@', '%@', '%@') VALUES ('%@', '%@', '%@')",  
                              TABLENAME, NAME, AGE, ADDRESS, @"张三", @"13", @"济南"];  
       BOOL res = [db executeUpdate:insertSql1];  
       NSString *insertSql2 = [NSString stringWithFormat:  
                               @"INSERT INTO '%@' ('%@', '%@', '%@') VALUES ('%@', '%@', '%@')",  
                               TABLENAME, NAME, AGE, ADDRESS, @"李四", @"12", @"济南"];  
       BOOL res2 = [db executeUpdate:insertSql2];  
         
       if (!res) {  
           NSLog(@"error when insert db table");  
       } else {  
           NSLog(@"success to insert db table");  
       }  
       [db close];  
  
   }  

修改数据

if ([db open]) {  
        NSString *updateSql = [NSString stringWithFormat:  
                               @"UPDATE '%@' SET '%@' = '%@' WHERE '%@' = '%@'",  
                               TABLENAME,   AGE,  @"15" ,AGE,  @"13"];  
        BOOL res = [db executeUpdate:updateSql];  
        if (!res) {  
            NSLog(@"error when update db table");  
        } else {  
            NSLog(@"success to update db table");  
        }  
        [db close];  
  
    }  

删除数据

if ([db open]) {  
          
        NSString *deleteSql = [NSString stringWithFormat:  
                               @"delete from %@ where %@ = '%@'",  
                               TABLENAME, NAME, @"张三"];  
        BOOL res = [db executeUpdate:deleteSql];  
          
        if (!res) {  
            NSLog(@"error when delete db table");  
        } else {  
            NSLog(@"success to delete db table");  
        }  
        [db close];  
  
    }  

6.查询

查询操作使用了executeQuery,并涉及到FMResultSet。


if ([db open]) {  
        NSString * sql = [NSString stringWithFormat:  
                          @"SELECT * FROM %@",TABLENAME];  
        FMResultSet * rs = [db executeQuery:sql];  
        while ([rs next]) {  
            int Id = [rs intForColumn:ID];  
            NSString * name = [rs stringForColumn:NAME];  
            NSString * age = [rs stringForColumn:AGE];  
            NSString * address = [rs stringForColumn:ADDRESS];  
            NSLog(@"id = %d, name = %@, age = %@  address = %@", Id, name, age, address);  
        }  
        [db close];  
    }  

FMDB的FMResultSet提供了多个方法来获取不同类型的数据:

//FMResultSet has many methods to retrieve data in an 
//appropriate format:
intForColumn:
longForColumn:
longLongIntForColumn:
boolForColumn:
doubleForColumn:
stringForColumn:
dateForColumn:
dataForColumn:
dataNoCopyForColumn:
UTF8StringForColumnName:
objectForColumnName:

7.多线程操作数据库

如果应用中使用了多线程操作数据库,那么就需要使用FMDatabaseQueue来保证线程安全了。 应用中不可在多个线程中共同使用一个FMDatabase对象操作数据库,这样会引起数据库数据混乱。 为了多线程操作数据库安全,FMDB使用了FMDatabaseQueue,使用FMDatabaseQueue很简单,首先用一个数据库文件地址来初使化FMDatabaseQueue,然后就可以将一个闭包(block)传入inDatabase方法中。 在闭包中操作数据库,而不直接参与FMDatabase的管理。

FMDatabaseQueue * queue = [FMDatabaseQueue databaseQueueWithPath:database_path];  
   dispatch_queue_t q1 = dispatch_queue_create("queue1", NULL);  
   dispatch_queue_t q2 = dispatch_queue_create("queue2", NULL);  
     
   dispatch_async(q1, ^{  
       for (int i = 0; i < 50; ++i) {  
           [queue inDatabase:^(FMDatabase *db2) {  
                 
               NSString *insertSql1= [NSString stringWithFormat:  
                                      @"INSERT INTO '%@' ('%@', '%@', '%@') VALUES (?, ?, ?)",  
                                      TABLENAME, NAME, AGE, ADDRESS];  
                 
               NSString * name = [NSString stringWithFormat:@"jack %d", i];  
               NSString * age = [NSString stringWithFormat:@"%d", 10+i];  
                 
                 
               BOOL res = [db2 executeUpdate:insertSql1, name, age,@"济南"];  
               if (!res) {  
                   NSLog(@"error to inster data: %@", name);  
               } else {  
                   NSLog(@"succ to inster data: %@", name);  
               }  
           }];  
       }  
   });  
     
   dispatch_async(q2, ^{  
       for (int i = 0; i < 50; ++i) {  
           [queue inDatabase:^(FMDatabase *db2) {  
               NSString *insertSql2= [NSString stringWithFormat:  
                                      @"INSERT INTO '%@' ('%@', '%@', '%@') VALUES (?, ?, ?)",  
                                      TABLENAME, NAME, AGE, ADDRESS];  
                 
               NSString * name = [NSString stringWithFormat:@"lilei %d", i];  
               NSString * age = [NSString stringWithFormat:@"%d", 10+i];  
                 
               BOOL res = [db2 executeUpdate:insertSql2, name, age,@"北京"];  
               if (!res) {  
                   NSLog(@"error to inster data: %@", name);  
               } else {  
                   NSLog(@"succ to inster data: %@", name);  
               }  
           }];  
       }  
   });  

如有需要了解swift FMDB的使用或者更深入了解这个框架请上 GitHub连接

好了数据库就到这里,基本够用了。

码字不易,点个喜欢吧。

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

推荐阅读更多精彩内容