Laravel Database——查询构造器与语法编译器源码分析 (中)

join 语句

join 语句对数据库进行连接操作,join 函数的连接条件可以非常简单:

DB::table('services')->select('*')->join('translations AS t', 't.item_id', '=', 'services.id');

也可以比较复杂:

DB::table('users')->select('*')->join('contacts', function ($j) {

        $j->on('users.id', '=', 'contacts.id')->orOn('users.name', '=', 'contacts.name');

    });

    //select * from "users" inner join "contacts" on "users"."id" = "contacts"."id" or "users"."name" = "contacts"."name"

    $builder = $this->getBuilder();

    DB::table('users')->select('*')->from('users')->joinWhere('contacts', 'col1', function ($j) {

        $j->select('users.col2')->from('users')->where('users.id', '=', 'foo')

    });

    //select * from "users" inner join "contacts" on "col1" = (select "users"."col2" from "users" where "users"."id" = foo)

还可以更加复杂:

DB::table('users')->select('*')->leftJoin('contacts', function ($j) {

        $j->on('users.id', '=', 'contacts.id')->where(function ($j) {

            $j->where('contacts.country', '=', 'US')->orWhere('contacts.is_partner', '=', 1);

        });

    });

    //select * from "users" left join "contacts" on "users"."id" = "contacts"."id" and ("contacts"."country" = 'US' or "contacts"."is_partner" = 1)

    DB::table('users')->select('*')->leftJoin('contacts', function ($j) {

        $j->on('users.id', '=', 'contacts.id')->where('contacts.is_active', '=', 1)->orOn(function ($j) {

            $j->orWhere(function ($j) {

                $j->where('contacts.country', '=', 'UK')->orOn('contacts.type', '=', 'users.type');

            })->where(function ($j) {

                $j->where('contacts.country', '=', 'US')->orWhereNull('contacts.is_partner');

            });

        });

    });

    //select * from "users" left join "contacts" on "users"."id" = "contacts"."id" and "contacts"."is_active" = 1 or (("contacts"."country" = 'UK' or "contacts"."type" = "users"."type") and ("contacts"."country" = 'US' or "contacts"."is_partner" is null))

其实 join 语句与 where 语句非常相似,将 join 语句的连接条件看作 where 的查询条件完全可以,接下来我们看看源码。

join 语句

从上面的示例代码可以看出,join 函数的参数多变,第二个参数可以是列名,也有可能是闭包函数。当第二个参数是列名的时候,第三个参数可以是闭包,还可以是符号 =、>=。

public function join($table, $first, $operator = null, $second = null, $type = 'inner', $where = false)

{

    $join = new JoinClause($this, $type, $table);

    if ($first instanceof Closure) {

        call_user_func($first, $join);

        $this->joins[] = $join;

        $this->addBinding($join->getBindings(), 'join');

    }

    else {

        $method = $where ? 'where' : 'on';

        $this->joins[] = $join->$method($first, $operator, $second);

        $this->addBinding($join->getBindings(), 'join');

    }

    return $this;

}

可以看到,程序首先新建了一个 JoinClause 类对象,这个类实际上继承 queryBuilder,也就是说 queryBuilder 上的很多方法它都可以直接用,例如 where、whereNull、whereDate 等等。

class JoinClause extends Builder

{

}

如果第二个参数是闭包函数的话,就会像查询组一样根据查询条件更新 $join。

如果第二个参数是列名,那么就会调用 on 方法或 where 方法。这两个方法的区别是,on 方法只支持 whereColumn 方法和 whereNested,也就是说只能写出 join on col1 = col2 这样的语句,而 where 方法可以传递数组、子查询等等.

public function on($first, $operator = null, $second = null, $boolean = 'and')

{

    if ($first instanceof Closure) {

        return $this->whereNested($first, $boolean);

    }

    return $this->whereColumn($first, $operator, $second, $boolean);

}

public function orOn($first, $operator = null, $second = null)

{

    return $this->on($first, $operator, $second, 'or');

}

grammer——compileJoins

接下来我们来看看如何编译 join 语句:

protected function compileJoins(Builder $query, $joins)

{

    return collect($joins)->map(function ($join) use ($query) {

        $table = $this->wrapTable($join->table);

        return trim("{$join->type} join {$table} {$this->compileWheres($join)}");

    })->implode(' ');

}

可以看到,JoinClause 在编译中是作为 queryBuild 对象来看待的。


union 语句

union 用于合并两个或多个 SELECT 语句的结果集。Union 因为要进行重复值扫描,所以效率低。如果合并没有刻意要删除重复行,那么就使用 Union All。

我们在 laravel 中可以这样使用:

$query = DB::table('users')->select('*')->where('id', '=', 1);

$query->union(DB::table('users')->select('*')->where('id', '=', 2));

//(select * from `users` where `id` = 1) union (select * from `users` where `id` = 2)

还可以添加多个 union 语句:

$query = DB::table('users')->select('*')->where('id', '=', 1);

$query->union(DB::table('users')->select('*')->where('id', '=', 2));

$query->union(DB::table('users')->select('*')->where('id', '=', 3));     

//(select * from "users" where "id" = 1) union (select * from "users" where "id" = 2) union (select * from "users" where "id" = 3)

union 语句可以与 orderBy 相结合:

$query = DB::table('users')->select('*')->where('id', '=', 1);

$query->union(DB::table('users')->select('*')->where('id', '=', 2));

$query->orderBy('id', 'desc');

//(select * from `users` where `id` = ?) union (select * from `users` where `id` = ?) order by `id` desc

union 语句可以与 limit、offset 相结合:

$query = DB::table('users')->select('*');

$query->union(DB::table('users')->select('*'));

$builder->skip(5)->take(10);

//(select * from `users`) union (select * from `dogs`) limit 10 offset 5

union 函数

union 函数比较简单:

public function union($query, $all = false)

{

    if ($query instanceof Closure) {

        call_user_func($query, $query = $this->newQuery());

    }

    $this->unions[] = compact('query', 'all');

    $this->addBinding($query->getBindings(), 'union');

    return $this;

}

grammer——compileUnions

语法编译器对 union 的处理:

public function compileSelect(Builder $query)

{

    $sql = parent::compileSelect($query);

    if ($query->unions) {

        $sql = '('.$sql.') '.$this->compileUnions($query);

    }

    return $sql;

}

protected function compileUnions(Builder $query)

{

    $sql = '';

    foreach ($query->unions as $union) {

        $sql .= $this->compileUnion($union);

    }

    if (! empty($query->unionOrders)) {

        $sql .= ' '.$this->compileOrders($query, $query->unionOrders);

    }

    if (isset($query->unionLimit)) {

        $sql .= ' '.$this->compileLimit($query, $query->unionLimit);

    }

    if (isset($query->unionOffset)) {

        $sql .= ' '.$this->compileOffset($query, $query->unionOffset);

    }

    return ltrim($sql);

}

protected function compileUnion(array $union)

{

    $conjuction = $union['all'] ? ' union all ' : ' union ';

    return $conjuction.'('.$union['query']->toSql().')';

}

可以看出,union 的处理比较简单,都是调用 query->toSql 语句而已。值得注意的是,在处理 union 的时候,要特别处理 order、limit、offset。


orderBy 语句

orderBy 语句用法很简单,可以设置多个排序字段,也可以用原生排序语句:

DB::table('users')->select('*')->orderBy('email')->orderBy('age', 'desc');

DB::table('users')->select('*')->orderBy('email')->orderByRaw('age desc');

orderBy 函数

如果当前查询中有 union 的话,排序的变量会被放入 unionOrders 数组中,这个数组只有在 compileUnions 函数中才会被编译成 sql 语句。否则会被放入 orders 数组中,这时会被 compileOrders 处理:

public function orderBy($column, $direction = 'asc')

{

    $this->{$this->unions ? 'unionOrders' : 'orders'}[] = [

        'column' => $column,

        'direction' => strtolower($direction) == 'asc' ? 'asc' : 'desc',

    ];

    return $this;

}

grammer——compileOrders

orderBy 的编译也很简单:

protected function compileOrders(Builder $query, $orders)

{

    if (! empty($orders)) {

        return 'order by '.implode(', ', $this->compileOrdersToArray($query, $orders));

    }

    return '';

}

protected function compileOrdersToArray(Builder $query, $orders)

{

    return array_map(function ($order) {

        return ! isset($order['sql'])

                    ? $this->wrap($order['column']).' '.$order['direction']

                    : $order['sql'];

    }, $orders);

}


limit offset forPage 语句

limit offset 或者 skip take 用法很简单,有趣的是,laravel 考虑了负数的情况:

DB::select('*')->from('users')->offset(5)->limit(10);

DB::select('*')->from('users')->skip(5)->take(10);

DB::select('*')->from('users')->skip(-5)->take(-10);

DB::select('*')->from('users')->forPage(5, 10);

limit offset 函数

和 orderBy 一样,如果当前查询中有 union 的话,limit /offset 会被放入 unionLimit /unionOffset 中,在编译 union 的时候解析:

public function take($value)

{

    return $this->limit($value);

}

public function limit($value)

{

    $property = $this->unions ? 'unionLimit' : 'limit';

    if ($value >= 0) {

        $this->$property = $value;

    }

    return $this;

}

public function skip($value)

{

    return $this->offset($value);

}

public function offset($value)

{

    $property = $this->unions ? 'unionOffset' : 'offset';

    $this->$property = max(0, $value);

    return $this;

}

public function forPage($page, $perPage = 15)

{

    return $this->skip(($page - 1) * $perPage)->take($perPage);

}

grammer——compileLimit compileOffset

这个不能再简单了:

protected function compileLimit(Builder $query, $limit)

{

    return 'limit '.(int) $limit;

}

protected function compileOffset(Builder $query, $offset)

{

    return 'offset '.(int) $offset;

}


group 语句

groupBy 语句的参数形式有多种:

DB::select('*')->from('users')->groupBy('email');

DB::select('*')->from('users')->groupBy('id', 'email');

DB::select('*')->from('users')->groupBy(['id', 'email']);

DB::select('*')->from('users')->groupBy(new Raw('DATE(created_at)'));

groupBy 函数很简单,仅仅是为 $this->groups 成员变量合并数组:

public function groupBy(...$groups)

{

    foreach ($groups as $group) {

        $this->groups = array_merge(

            (array) $this->groups,

            Arr::wrap($group)

        );

    }

    return $this;

}

语法编译器的处理:

protected function compileGroups(Builder $query, $groups)

{

    return 'group by '.$this->columnize($groups);

}


having 语句

having 语句的用法也很简单。大致有 having、orHaving、havingRaw、orHavingRaw 这几个函数:

DB::select('*')->from('users')->having('email', '>', 1);

DB::select('*')->from('users')->groupBy('email')->having('email', '>', 1);

DB::select('*')->from('users')->having('email', 1)->orHaving('email', 2);

DB::select('*')->from('users')->havingRaw('user_foo < user_bar');

DB::select('*')->from('users')->having('baz', '=', 1)->orHavingRaw('user_foo < user_bar');

having 函数

having 函数大致与 whereColumn 相同:

public function having($column, $operator = null, $value = null, $boolean = 'and')

{

    $type = 'Basic';

    list($value, $operator) = $this->prepareValueAndOperator(

        $value, $operator, func_num_args() == 2

    );

    if ($this->invalidOperator($operator)) {

        list($value, $operator) = [$operator, '='];

    }

    $this->havings[] = compact('type', 'column', 'operator', 'value', 'boolean');

    if (! $value instanceof Expression) {

        $this->addBinding($value, 'having');

    }

    return $this;

}

havingRaw 函数:

public function havingRaw($sql, array $bindings = [], $boolean = 'and')

{

    $type = 'Raw';

    $this->havings[] = compact('type', 'sql', 'boolean');

    $this->addBinding($bindings, 'having');

    return $this;

}

grammer——compileHavings

语法编译器:

protected function compileHavings(Builder $query, $havings)

{

    $sql = implode(' ', array_map([$this, 'compileHaving'], $havings));

    return 'having '.$this->removeLeadingBoolean($sql);

}

protected function compileHaving(array $having)

{

    if ($having['type'] === 'Raw') {

        return $having['boolean'].' '.$having['sql'];

    }

    return $this->compileBasicHaving($having);

}

protected function compileBasicHaving($having)

{

    $column = $this->wrap($having['column']);

    $parameter = $this->parameter($having['value']);

    return $having['boolean'].' '.$column.' '.$having['operator'].' '.$parameter;

}


when /tap/unless 语句

when 语句可以根据条件来判断是否执行查询条件,unless 与 when 相反,第一个参数是 false 才会调用闭包函数执行查询,tap 指定 when 的第一参数永远为真:

$callback = function ($query, $condition) {

    $this->assertEquals($condition, 'truthy');

    $query->where('id', '=', 1);

};

$default = function ($query, $condition) {

    $this->assertEquals($condition, 0);

    $query->where('id', '=', 2);

};

DB::select('*')->from('users')->when('truthy', $callback, $default)->where('email', 'foo');

DB::select('*')->from('users')->tap($callback)->where('email', 'foo');

DB::select('*')->from('users')->unless('truthy', $callback, $default)->where('email', 'foo');

when、unless、tap 函数的实现:

public function when($value, $callback, $default = null)

{

    if ($value) {

        return $callback($this, $value) ?: $this;

    } elseif ($default) {

        return $default($this, $value) ?: $this;

    }

    return $this;

}

public function unless($value, $callback, $default = null)

{

    if (! $value) {

        return $callback($this, $value) ?: $this;

    } elseif ($default) {

        return $default($this, $value) ?: $this;

    }

    return $this;

}

public function tap($callback)

{

    return $this->when(true, $callback);

}


Aggregate 查询

聚合方法也是 sql 的重要组成部分,laravel 提供 count、max、min、avg、sum、exist 等聚合方法:

DB::table('users')->count();//select count(*) as aggregate from "users"

DB::table('users')->max('id');//select max("id") as aggregate from "users"

DB::table('users')->min('id');//select min("id") as aggregate from "users"

DB::table('users')->sum('id');//select sum("id") as aggregate from "users"

DB::table('users')->exists();//select exists(select * from "users") as "exists"

这些聚合函数实际上都是调用 aggregate 函数:

public function count($columns = '*')

{

    return (int) $this->aggregate(__FUNCTION__, Arr::wrap($columns));

}

public function aggregate($function, $columns = ['*'])

{

    $results = $this->cloneWithout(['columns'])

                    ->cloneWithoutBindings(['select'])

                    ->setAggregate($function, $columns)

                    ->get($columns);

    if (! $results->isEmpty()) {

        return array_change_key_case((array) $results[0])['aggregate'];

    }

}

可以看出来,aggregate 函数复制了一份 queryBuilder,只是缺少了 select、bingding 成员变量:

public function cloneWithout(array $properties)

{

    return tap(clone $this, function ($clone) use ($properties) {

        foreach ($properties as $property) {

            $clone->{$property} = null;

        }

    });

}

public function cloneWithoutBindings(array $except)

{

    return tap(clone $this, function ($clone) use ($except) {

        foreach ($except as $type) {

            $clone->bindings[$type] = [];

        }

    });

}

protected function setAggregate($function, $columns)

{

    $this->aggregate = compact('function', 'columns');

    if (empty($this->groups)) {

        $this->orders = null;

        $this->bindings['order'] = [];

    }

    return $this;

}

exist 聚合函数和其他不一样,它的流程与 whereExist 大致相同:

public function exists()

{

    $results = $this->connection->select(

        $this->grammar->compileExists($this), $this->getBindings(), ! $this->useWritePdo

    );

    if (isset($results[0])) {

        $results = (array) $results[0];

        return (bool) $results['exists'];

    }

    return false;

}

grammer——compileAggregate

laravel 的聚合函数具有独占性,也就是说调用聚合函数后,不能再 select 其他的列:

protected function compileAggregate(Builder $query, $aggregate)

{

    $column = $this->columnize($aggregate['columns']);

    if ($query->distinct && $column !== '*') {

        $column = 'distinct '.$column;

    }

    return 'select '.$aggregate['function'].'('.$column.') as aggregate';

}

protected function compileColumns(Builder $query, $columns)

{

    if (! is_null($query->aggregate)) {

        return;

    }

    $select = $query->distinct ? 'select distinct ' : 'select ';

    return $select.$this->columnize($columns);

}

可以看到,如果存在聚合函数,那么编译 select 的 compileColumns 函数将不会运行。


first / find / value / pluck / implode

从数据库中取出后数据,我们可以使用 laravel 提供给我们的一些函数进行包装处理。

first 函数可以让我们只查询第一条:

DB::table('users')->where('id', '=', 1)->first();

find 函数,可以利用数据库表的主键来查询第一条:

DB::table('users')->find(1);

pluck 函数可以取查询记录的某一列:

DB::table('users')->where('id', '=', 1)->pluck('foo');/['bar', 'baz']

pluck 函数取查询记录的某一列的同时,还可以设置列名的 key:

DB::table('users')->where('id', '=', 1)->pluck('foo', 'id');//[1 => 'bar', 10 => 'baz']

value 函数可以取第一条数据的某一列:

DB::table('users')->where('id', '=', 1)->value('foo');//bar

implode 函数可以将多条数据的某一列拼成字符串:

DB::table('users')->where('id', '=', 1)->implode('foo', ',');//'bar,baz'

first 函数

find 函数,使用了 limit 1 的 sql 语句:

public function first($columns = ['*'])

{

    return $this->take(1)->get($columns)->first();

}

find 函数

find 函数实际利用主键调用 first 函数:

public function find($id, $columns = ['*'])

{

    return $this->where('id', '=', $id)->first($columns);

}

pluck 函数

pluck 函数主要对得到的数据调用 pluck 函数:

public function pluck($column, $key = null)

{

    $results = $this->get(is_null($key) ? [$column] : [$column, $key]);

    return $results->pluck(

        $this->stripTableForPluck($column),

        $this->stripTableForPluck($key)

    );

}

implod 函数

implod 函数对一维数组调用 implod 函数:

public function implode($column, $glue = '')

{

    return $this->pluck($column)->implode($glue);

}


chunk 语句

如果你需要操作数千条数据库记录,可以考虑使用 chunk 方法。这个方法每次只取出一小块结果,并会将每个块传递给一个闭包处理。

DB::table('users')->orderBy('id')->chunk(100, function ($users) {

    foreach ($users as $user) {

        //

    }

});

你可以从 闭包 中返回 false,以停止对后续分块的处理:

DB::table('users')->orderBy('id')->chunk(100, function ($users) {

    // Process the records...

    if (...) {

        return false;

    }

});

如果不想按照主键 id 来进行分块,我们还可以自定义分块主键:

DB::table('users')->orderBy('id')->chunkById(100, function ($users) {

    foreach ($users as $user) {

        //

    }

}, 'someIdField');

chunk 函数

chunk 函数的实现实际上是 forPage 函数,当从数据库获得数据后,先判断是否拿到了数据,如果拿到了就会继续执行闭包函数,否则就会中断程序。执行闭包函数后,需要判断返回状态。若取出的数据小于分块的条数,说明数据已经全部获取完毕,结束程序。

public function chunk($count, callable $callback)

{

    $this->enforceOrderBy();

    $page = 1;

    do {

        $results = $this->forPage($page, $count)->get();

        $countResults = $results->count();

        if ($countResults == 0) {

            break;

        }

        if ($callback($results, $page) === false) {

            return false;

        }

        unset($results);

        $page++;

    } while ($countResults == $count);

    return true;

}

protected function enforceOrderBy()

{

    if (empty($this->query->orders) && empty($this->query->unionOrders)) {

        $this->orderBy($this->model->getQualifiedKeyName(), 'asc');

    }

}

enforceOrderBy 函数是用于数据按照主键的大小进行排序。

chunkById 函数

chunkById 函数与 chunk 函数唯一不同的是 forPage 函数被换成了 forPageAfterId 函数,目的是替换主键:

public function chunkById($count, callable $callback, $column = 'id', $alias = null)

{

    $alias = $alias ?: $column;

    $lastId = 0;

    do {

        $clone = clone $this;

        $results = $clone->forPageAfterId($count, $lastId, $column)->get();

        $countResults = $results->count();

        if ($countResults == 0) {

            break;

        }

        if ($callback($results) === false) {

            return false;

        }

        $lastId = $results->last()->{$alias};

        unset($results);

    } while ($countResults == $count);

    return true;

}

forPageAfterId 函数实际上是把 offset 函数删除,并按照自定义的列来排序,每次获取最后一条数据的自定义列的数值,利用 where 条件不断获取下一部分分块数据:

public function forPageAfterId($perPage = 15, $lastId = 0, $column = 'id')

{

    $this->orders = $this->removeExistingOrdersFor($column);

    return $this->where($column, '>', $lastId)

                ->orderBy($column, 'asc')

                ->take($perPage);

}

protected function removeExistingOrdersFor($column)

{

    return Collection::make($this->orders)

                ->reject(function ($order) use ($column) {

                    return isset($order['column'])

                          ? $order['column'] === $column : false;

                })->values()->all();

}

本作品采用《CC 协议》,转载必须注明作者和本文链接

————————————————

原文作者:leoyang

转自链接:https://learnku.com/articles/6249/laravel-database-query-constructor-and-syntax-compiler-source-code-analysis-in

版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请保留以上作者信息和原文链接。

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

推荐阅读更多精彩内容