4.2.2 SQLite数据库的增删改查和事务

本节例程下载地址:WillFlowDatabast

通过上一篇我们已经掌握了创建和升级数据库的方法,接下来就该学习一下如何对表中的数据进行操作了。其实我们可以对数据进行的操作也就无非四种,即 CRUD。其中 C 代表添加( Create), R 代表查询( Retrieve), U 代表更新( Update), D 代表删除( Delete)。每一种操作又各自对应了一种 SQL 命令,如果你比较熟悉 SQL 语言的话,一定会知道添加数据时使用 insert,查询数据时使用 select,更新数据时使用 update,删除数据时使用 delete。但是开发者的水平总会是参差不齐的,未必每一个人都能非常熟悉地使用 SQL 语言,因此 Android也是提供了一系列的辅助性方法,使得在 Android 中即使不去编写 SQL 语句,也能轻松完成所有的 CRUD 操作。

通过前面的学习我们已经知道,调用 SQLiteOpenHelper 的 getReadableDatabase() 或 getWritableDatabase() 方法是可以用于创建和升级数据库的,不仅如此,这两个方法还都会返回一个 SQLiteDatabase 对象,借助这个对象就可以对数据进行 CRUD 操作了。

一、添加数据

首先学习一下如何向数据库的表中添加数据。SQLiteDatabase 中提供了一个 insert() 方法,这个方法就是专门用于添加数据的。它接收三个参数,第一个参数是表名,我们希望向哪张表里添加数据,这里就传入该表的名字。第二个参数用于在未指定添加数据的情况下给某些可为空的列自动赋值 NULL,一般我们用不到这个功能,直接传入 null 即可。第三个参数是一个 ContentValues 对象,它提供了一系列的 put()方法重载,用于向 ContentValues 中添加数据,只需要将表中的每个列名以及相应的待添加数据传入即可。接下来还是让我们通过例子的方式来亲身体验一下如何添加数据。

修改 activity_main.xml 中的代码,如下所示:
    <Button
        android:id="@+id/buttonAddDataBase"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="添加数据"
        android:textColor="#9eff00"
        android:textSize="25dp" />
接着修改 MainActivity 中的代码,如下所示:
        mButtonAdd = (Button) findViewById(R.id.buttonAddDataBase);
        mButtonAdd.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                SQLiteDatabase sqLiteDatabase = myDatabaseHelper.getWritableDatabase();
                ContentValues values = new ContentValues();
                // 开始组装第一条数据
                values.put("name", "WillFlow");
                values.put("author", "WGH");
                values.put("pages", 525);
                values.put("price", 16.18);
                sqLiteDatabase.insert("Book", null, values); // 插入第一条数据
                values.clear();
                // 开始组装第二条数据
                values.put("name", "Flipped");
                values.put("author", "Rob Reiner");
                values.put("pages", 678);
                values.put("price", 20.17);
                sqLiteDatabase.insert("Book", null, values); // 插入第二条数据
            }
        });

在添加数据按钮的点击事件里面,我们先获取到了 SQLiteDatabase 对象,然后使用 ContentValues 来对要添加的数据进行组装。这里只对 Book 表里其中四列的数据进行了组装,id 那一列没并没给它赋值。这是因为在前面创建表的时候我们就将 id 列设置为自增长了,它的值会在入库的时候自动生成,所以不需要手动给它赋值了。接下来调用了 insert() 方法将数据添加到表当中,注意这里我们实际上添加了两条数据,上述代码中使用 ContentValues 分别组装了两次不同的内容,并调用了两次 insert() 方法。

编译运行,效果如下:

为了证实一下,我们点击添加数据按钮,然后打开 BookStore.db 数据库,输入 SQL 查询语句 select * from Book;,结果如图:


由此可以看出,我们刚刚点击按钮所创建的两条数据,都已经准确无误地添加到 Book 表中了。

二、更新数据

SQLiteDatabase 中提供了一个非常好用的 update() 方法用于对数据进行更新,这个方法接收四个参数,第一个参数和 insert() 方法一样,也是表名,在这里指定去更新哪张表里的数据。第二个参数是 ContentValues 对象,要把更新数据在这里组装进去。第三、第四个参数用于去约束更新某一行或某几行中的数据,不指定的话默认就是更新所有行。那么接下来我们看一下更新数据的具体用法。

修改 activity_main.xml 中的代码,如下所示:
    <Button
        android:id="@+id/buttonUpdataDataBase"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="更新数据"
        android:textColor="#ff9100"
        android:textSize="25dp" />
修改 MainActivity 中的代码,如下所示:
        mButtonUpgrade.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                SQLiteDatabase sqLiteDatabase = myDatabaseHelper.getWritableDatabase();
                ContentValues values = new ContentValues();
                values.put("author", "Rob");
                sqLiteDatabase.update("Book", values, "name = ?", new String[] { "Flipped" });
            }
        });

这里在更新数据按钮的点击事件里面构建了一个 ContentValues 对象,并且只给它指定了一组数据,说明我们只是想把作者这一列的数据更新成"Rob"。然后调用了 SQLiteDatabase 的 update()方法去执行具体的更新操作。这里使用了第三、第四个参数来指定具体更新哪几行:第三个参数对应的是 SQL 语句的 where 部分,表示去更新所有 name 等于?的行,而?是一个占位符,可以通过第四个参数提供的一个字符串数组为第三个参数中的每个占位符指定相应的内容。因此上述代码想表达的意图就是:将名字是 Flipped 的这本书的作者改成 Rob。

编译运行并查看效果:

可以看到 Flipped 这本书的作者已经被成功改为 Rob 了。

三、删除数据

SQLiteDatabase 中提供了一个 delete() 方法专门用于删除数据,这个方法接收三个参数,第一
个参数仍然是表名,第二、第三个参数又是用于去约束删除某一行或某几行的数据,不指定的话默认就是删除所有行。

修改 activity_main.xml 中的代码,如下所示:
    <Button
        android:id="@+id/buttonDeleteDataBase"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="删除数据"
        android:textColor="#ff3700"
        android:textSize="25dp" />
然后修改 MainActivity 中的代码,如下所示:
        mButtonDelete.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                SQLiteDatabase sqLiteDatabase = myDatabaseHelper.getWritableDatabase();
                sqLiteDatabase.delete("Book", "pages > ?", new String[] { "600" });
            }
        });

我们在删除按钮的点击事件里指明去删除 Book 表中的数据,并且通过第二、第三个参数来指定仅删除那些页数超过 500 页的书籍。当然这个需求很奇怪,这里也仅仅是为了做个测试。你可以先查看一下当前 Book 表里的数据,其中 Flipped 这本书的页数超过了 600 页,也就是说当我们点击删除按钮时,这条记录应该会被删除掉。

编译运行并查看效果:

四、查询数据

我们都知道 SQL 的全称是 Structured Query Language,翻译成中文就是结构化查询语言。它的大部功能都是体现在“查”这个字上的,而“增删改”只是其中的一小部分功能。由于 SQL 查询涉及的内容实在是太多了,因此在这里我不准备对它展开来讲解,而是只会介绍 Android 上的查询功能。如果你对 SQL 语言非常感兴趣,可以找一本专门介绍 SQL 的书进行学习。我们这里用到的是 SQLiteDatabase 中还提供的一个 query() 方法来于对数据进行查询。

这个方法的参数非常复杂,最短的一个方法重载也需要传入七个参数。那我们就先来看一下这七个参数各自的含义吧,第一个参数不用说,当然还是表名,表示我们希望从哪张表中查询数据。第二个参数用于指定去查询哪几列,如果不指定则默认查询所有列。第三、第四个参数用于去约束查询某一行或某几行的数据,不指定则默认是查询所有行的数据。第五个参数用于指定需要去 group by 的列,不指定则表示不对查询结果进行 group by 操作。第六个参数用于对 group by 之后的数据进行进一步的过滤,不指定则表示不进行过滤。第七个参数用于指定查询结果的排序方式,不指定则表示使用默认的排序方式。其他几个 query()方法的重载其实也大同小异,更多详细的内容可以参考下表:

query()方法参数 对应 SQL 部分 描述
table from table_name 指定查询的表名
columns select column1, column2 指定查询的列名
selection where column = value 指定 where 的约束条件
selectionArgs --- 为 where 中的占位符提供具体的值
groupBy group by column 指定需要 group by 的列
having having column = value 对 group by 后的结果进一步约束
orderBy order by column1, column2 指定查询结果的排序方式

虽然 query() 方法的参数非常多,但是不要对它产生畏惧,因为我们不必为每条查询语句都指定上所有的参数,多数情况下只需要传入少数几个参数就可以完成查询操作了。调用 query() 方法后会返回一个 Cursor 对象,查询到的所有数据都将从这个对象中取出。下面我们通过例子的方式来体验一下查询数据的具体用法。

修改 activity_main.xml 中的代码,如下所示:
    <Button
        android:id="@+id/buttonQueryDataBase"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="查询数据"
        android:textColor="#ae00ff"
        android:textSize="25dp" />
然后修改 MainActivity 中的代码,如下所示:
        mButtonQuery.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                SQLiteDatabase sqLiteDatabase = myDatabaseHelper.getWritableDatabase();
                // 查询Book表中所有的数据
                Cursor cursor = sqLiteDatabase.query("Book", null, null, null, null, null, null);
                if (cursor != null) {
                    while (cursor.moveToNext()) {   // 遍历Cursor对象,取出数据并打印
                        String name = cursor.getString(cursor.getColumnIndex("name"));
                        String author = cursor.getString(cursor.getColumnIndex("author"));
                        int pages = cursor.getInt(cursor.getColumnIndex("pages"));
                        double price = cursor.getDouble(cursor.getColumnIndex("price"));
                        Log.d(TAG, "book name : " + name + ", author : " + author + ", pages : " + pages + ", price : " + price);
                        Toast.makeText(MainActivity.this, "book name : " + name + ", author : " + author + ", pages : " + pages + ", price : " + price, Toast.LENGTH_SHORT).show();
                    }
                    cursor.close();
                }
            }
        });

我们首先在查询按钮的点击事件里面调用了 SQLiteDatabase 的 query() 方法去查询数据。这里的 query() 方法只是使用了第一个参数指明去查询 Book 表,后面的参数全部为 null。这就表示希望查询这张表中的所有数据,虽然这张表中目前只剩下一条数据了。查询完之后就得到了一个 Cursor 对象,接着我们调用 if (cursor != null) 判断是否成功获得 Cursor 对象,然后调用 while (cursor.moveToNext()) 遍历Cursor对象,在这个循环中可以通过 Cursor 的 getColumnIndex() 方法获取到某一列在表中对应的位置索引,然后将这个索引传入到相应的取值方法中,就可以得到从数据库中读取到的数据了。接着我们使用 Log 的方式将取出的数据打印出来并弹出吐司,借此来检查一下读取工作有没有成功完成。最后调用 close() 方法来关闭 Cursor。

编译运行看效果:
对应的Log信息为:
com.wgh.willflowDatabast D/MainActivity: book name : WillFlow, author : WGH, pages : 525, price : 16.18

这里已经将 Book 表中唯一的一条数据成功地读取出来了。当然这个例子只是对查询数据的用法进行了最简单的示范,在真正的项目中你可能会遇到比这要复杂得多的查询功能,更多高级的用法还需要我们自己去慢慢摸索,毕竟 query() 方法中还有那么多的参数我们都还没用到呢。

五、使用 SQL 操作数据库

虽然 Android 已经给我们提供了很多非常方便的 API 用于操作数据库,不过总会有一些人不习惯去使用这些辅助性的方法,而是更加青睐于直接使用 SQL 来操作数据库。幸运的是 Android 充分考虑到了我们的编程习惯,同样提供了一系列的方法,使得可以直接通过 SQL 来操作数据库。下面我们就来学习一下如何直接使用 SQL 来完成前面的 CRUD 操作。

添加数据的方法如下:
sqLiteDatabase.execSQL("insert into Book (name, author, pages, price) values(?, ?, ?, ?)", new String[] { "WillFlow", "WGH", "525", "16.18" });
更新数据的方法如下:
sqLiteDatabase.execSQL("update Book set author = ? where name = ?", new String[] { "Rob", "Flipped" });
删除数据的方法如下:
sqLiteDatabase.execSQL("delete from Book where pages > ?", new String[] { "600" });
查询数据的方法如下:
sqLiteDatabase.rawQuery("select * from Book", null);

除了查询数据的时候调用的是 SQLiteDatabase 的 rawQuery() 方法, 其他的操作都是调用的 execSQL() 方法。以上演示的几种方式,执行结果会和前面学习的 CRUD 操作的结果完全相同,选择使用哪一种方式就看个人的喜好了。

六、SQLite数据库的事务

SQLite 数据库是支持事务的,事务的特性可以保证让某一系列的操作要么全部完成,要么一个都不会完成。那么在什么情况下才需要使用事务呢?想象以下场景,比如你正在进行一次转账操作,银行会将转账的金额先从你的账户中扣除,然后再向收款方的账户中添加等量的金额,看上去好像没什么问题。可是如果当你账户中的金额刚刚被扣除,这时由于一些异常原因导致对方收款失败,这一部分钱就凭空消失了!当然银行肯定已经充分考虑到了这种情况,它会保证扣钱和收款的操作要么一起成功,要么都不会成功,而使用到的技术就是数据库的事务。

接下来我们看一看如何在 Android 中使用事务。比如 Book 表中的数据都已经很老了,现在准备全部废弃掉替换成新数据,可以先使用 delete() 方法将 Book 表中的数据删除,然后再使用 insert() 方法将新的数据添加到表中。我们要保证的是,删除旧数据和添加新数据的操作必须一起完成,否则就还要继续保留原来的旧数据。

修改 activity_main.xml 中的代码,如下所示:
    <Button
        android:id="@+id/buttonReplaceDataBase"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="替换数据"
        android:textColor="#e6ff00"
        android:textSize="25dp" />
然后修改 MainActivity 中的代码,如下所示:
        mButtonReplace.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                SQLiteDatabase sqLiteDatabase = myDatabaseHelper.getWritableDatabase();
                sqLiteDatabase.beginTransaction(); // 开启事务
                try {
                    sqLiteDatabase.delete("Book", null, null);
                    if (true) {
                        // 在这里手动抛出一个异常,让事务失败
                        throw new NullPointerException();
                    }
                    ContentValues values = new ContentValues();
                    values.put("name", "Games");
                    values.put("author", "Martin");
                    values.put("pages", 720);
                    values.put("price", 20.85);
                    sqLiteDatabase.insert("Book", null, values);
                    sqLiteDatabase.setTransactionSuccessful(); // 事务已经执行成功
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    sqLiteDatabase.endTransaction(); // 结束事务
                }
            }
        });

上述代码就是 Android中事务的标准用法,首先调用 SQLiteDatabase 的 beginTransaction() 方法来开启一个事务,然后在一个异常捕获的代码块中去执行具体的数据库操作,当所有的操作都完成之后,调用 setTransactionSuccessful() 表示事务已经执行成功了,最后在 finally 代码块中调用 endTransaction() 来结束事务。我们在删除旧数据的操作完成后手动抛出了一个 NullPointerException,这样添加新数据的代码就执行不到了。不过由于事务的存在,中途出现异常会导致事务的失败,此时旧数据应该是删除不掉的。

现在可以运行一下程序并点击替换数据按钮,你会发现, Book 表中存在的还是之前的旧数据。然后将手动抛出异常的那行代码去除,再重新运行一下程序,此时点击一下替换数据按钮就会将 Book 表中的数据替换成新数据了。

七、安全地升级数据库的方法

1、增加新表

上面我们学习的升级数据库的方式是非常粗暴的,为了保证数据库中的表是最新的, 我们只是简单地在 onUpgrade() 方法中删除掉了当前所有的表,然后强制重新执行了一遍 onCreate() 方法。 这种方式在产品的开发阶段确实可以用,但是当产品真正上线了之后就绝对不行了。比如你编写的某个应用已经成功上线,并且还拥有了不错的下载量。现在由于添加新功能的原因,使得数据库也需要一起升级,然后用户更新了这个版本之后发现以前程序中存储的本地数据全部丢失了!那么很遗憾,你的用户群体可能已经流失一大半了。

难道说在产品发布出去之后还不能升级数据库了吗? 当然不是,其实只需要进行一些合理的控制,就可以保证在升级数据库的时候数据并不会丢失了,下面我们来学习一下如何实现这样的功能。

我们已经知道,每一个数据库版本都会对应一个版本号,当指定的数据库版本号大于当前数据库版本号的时候,就会进入到 onUpgrade() 方法中去执行更新操作。这里需要为每一个版本号赋予它各自改变的内容,然后在 onUpgrade() 方法中对当前数据库的版本号进行判断,再执行相应的改变就可以了。

修改 MyDatabaseHelper 中的代码如下:
    @Override
    public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) {
        switch (oldVersion) {
            case 1:
                sqLiteDatabase.execSQL(CREATE_CATEGORY);
            default:
        }
        onCreate(sqLiteDatabase);
    }

我们在 onUpgrade() 方法中添加了一个 switch 判断,如果用户当前数据库的版本号是 1,就只会创建一张 Category 表。这样当用户是直接安装的第二版的程序时,就会将两张表一起创建。而当用户是使用第二版的程序覆盖安装第一版的程序时,就会进入到升级数据库的操作中,此时由于 Book 表已经存在了,因此只需要创建一张 Category 表即可。

2、创建关联

但是新的需求又来了,这次要给 Book 表和 Category 表之间建立关联,需要在 Book 表中添加一个 category_id 的字段,要怎么办呢?

修改 MyDatabaseHelper 中的代码,如下所示:
    private static final String CREATE_BOOK = "create table Book ("
            + "id integer primary key autoincrement, "
            + "author text, "
            + "price real, "
            + "pages integer, "
            + "name text, "
            + "category_id integer)";

    @Override
    public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) {
        switch (oldVersion) {
            case 1:
                sqLiteDatabase.execSQL(CREATE_CATEGORY);
            case 2:
                sqLiteDatabase.execSQL("alter table Book add column category_id integer");
            default:
        }
    }

首先我们在 Book 表的建表语句中添加了一个 category_id 列,这样当用户直接安装第三版的程序时,这个新增的列就已经自动添加成功了。然而,如果用户之前已经安装了某一版本的程序,现在需要覆盖安装,就会进入到升级数据库的操作中。在 onUpgrade() 方法里,我们添加了一个新的 case,如果当前数据库的版本号是 2,就会执行 alter 命令来为 Book 表新增一个 category_id 列。

这里请注意一个非常重要的细节, switch 中每一个 case 的最后都是没有使用 break 的,为什么要这么做呢?这是为了保证在跨版本升级的时候,每一次的数据库修改都能被全部执行到。比如用户当前是从第二版程序升级到第三版程序的,那么 case 2 中的逻辑就会执行。而如果用户是直接从第一版程序升级到第三版程序的,那么 case 1 和 case 2 中的逻辑都会执行。使用这种方式来维护数据库的升级,不管版本怎样更新,都可以保证数据库的表结构是最新的,而且表中的数据也完全不会丢失了。

八、常用方法汇总

创建数据库

public class DatabaseHelper extends SQLiteOpenHelper {
    private Context mContext;

    public DatabaseHelper(Context context, String name, CursorFactory factory, int version) {
        super(context, name, factory, version);
        mContext = context;
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        updateDatabase(db);
        DLog.d(TAG, "Create tables!");
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        // TODO Auto-generated method stub
    }

    private void updateDatabase(SQLiteDatabase db){

        // 路线信息表。
        db.execSQL("CREATE TABLE IF NOT EXISTS patrol_path (" +
                "_id INTEGER PRIMARY KEY," +
                "_path_id INTEGER," +
                "_path_name TEXT," +
                "description TEXT," +
                "startTime TEXT," +
                "remindTime TEXT," +
                "intervalTime TEXT," +
                "ordered TEXT," +
                "trajectory TEXT," +
                "createTime TEXT" +
                ");" );
    }
}

插入记录

    /**
     * 插入记录
     * @param   table           想插入数据的表名
     * @param   contentValues   一行记录的数据
     * @return  返回新纪录的行号,该行号是一个内部值,与主键id无关,错误时返回-1
     * @author  wgh
     * @time    16年11月7日
     */
    public long insertPatrolData(String table, ContentValues contentValues){
        // 第二个参数:代表强行插入null值的列名。当values参数为null或者不包含任何key-value对时该参数有效。
        long result = mPatrolDB.insert(table, null, contentValues);
        return result;
    }

更新记录

    /**
     * @param   table        想更新数据的表名
     * @param   values       想要更新的数据
     * @param   whereClause  满足该whereClause子句的记录将会被更新
     * @param   whereArgs    用于为whereClause子句传入参数
     * @return  返回受此update语句影响记录的条数
     * @author  wgh
     * @time    16年11月7日
     */
    public int updatePatrolData(String table, ContentValues values, String whereClause, String[] whereArgs){
        int result = mPatrolDB.update(table, values, whereClause, whereArgs);
        return result;
    }

删除记录

    /**
     * @param   table       想删除数据的表名
     * @param   whereClause 满足该whereClause子句的记录将会被删除
     * @param   whereArgs   用于为whereClause子句传入参数
     * @return  返回受此delete语句影响记录的条数
     * @author  wgh
     * @time    16年11月7日
     */
    public int deletePatrolData(String table,String whereClause, String[] whereArgs){
        int result = mPatrolDB.delete(table, whereClause, whereArgs);
        return result;
    }

查询记录

    /**
     * @param   distinct        指定是否去除重复记录
     * @param   table           执行查询数据的表名
     * @param   columns         要查询出来的列名
     * @param   whereClause     查询条件子句
     * @param   selectionArgs   用于为whereClause中的占位符传入参数
     * @param   groupBy         用于控制分组
     * @param   having          用于对分组进行过滤
     * @param   orderBy         用于对记录进行排序
     * @param   limit           用于进行分页
     * @return  游标
     * @author  wgh
     * @time    16年11月7日
     */
    public Cursor queryPatrolData(String table,String[] columns, String whereClause, String[] selectionArgs,
            String groupBy, String having, String orderBy, String limit){
        Cursor result = mPatrolDB.query(table, columns, whereClause, selectionArgs, null, null, orderBy, null);
//        Cursor result = mPatrolDB.query(Boolean distinct, table, columns, selection, selectionArgs, null, null, orderBy, null);
        return result;
    }
    /**
     * @param   sql     自己拼接的sqlite语句
     * @param   selectionArgs   用于为sql中的占位符传入参数
     * @author  wgh
     * @time    16年11月7日
     */
    public Cursor rawQuery(String sql, String[] selectionArgs) {
        Cursor result = mPatrolDB.rawQuery(sql, selectionArgs);
        return result;
    }

点此进入:GitHub开源项目“爱阅”

感谢优秀的你跋山涉水看到了这里,欢迎关注下让我们永远在一起!

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

推荐阅读更多精彩内容