Laravel Eloquent ORM 关系查询

索引
[隐藏]

在信息系统中有很多一对多的关系,常见的一个例子是一个订单(order)中包含多种商品(product)。我们通常会使用一个order表来存储订单,同时使用一个product表来存储商品,并在product表中加入order_id外键来将产品关联到订单中。

在以前开发项目时,我一直使用先查询order,再通过order_id查询product,并遍历查询到的order对象,将product插入order中的做法。但是这样做非常不优雅,并且会严重影响性能。同时,由于数据库中没有外键约束,一旦业务代码中出现bug,则会影响数据的一致性。

在这次的项目开发中,我使用Laravel的Eloquent ORM来实现外键的关系查询。

一些概念

我们首先来复习一些数据库系统导论中的一些概念

主键(Primary Key)

主关键字(Primary Key)是表中的一个或多个字段,它的值用于唯一地标识表中的某一条记录。主键通常名为id,并且为自增。

外键(Foreign Key)

外键(Foreign Key)的作用是建立两个表之间的关联。下面这张图可以直观地展示外键的作用。

外键

连接(Join)

连接(Join)将两张表中能关联起来的数据连接后返回。关于连接的更多内容请参考这篇文章:图解 SQL 里的各种 JOIN — 码志

Laravel的Eloquent ORM中并没有实现Join,如果需要在Laravel中使用Join则需要在Query Builder中完成。请注意不要将本文所述内容和join混淆,Eloquent中的with方法的实现是通过模型中定义的关系另外进行一次查询,并没有使用join。

场景

我们假设一个网上商城中,每个用户有若干个订单,每个订单中有若干个产品。我们需要实现在一次查询中,通过用户id查询其所有订单和及订单中的所有产品,并返回一个如下格式的json。

{"result":[{
            "id":50,
            "order_sn":"P6oz2yUWj2",
            "user_id":6,
            "shelf_id":34,
            "total_price":17,
            "pay_type":0,
            "transaction_no":"R4fl4NvG7m",
            "status":1,
            "created_at":"2019-01-17 20:39:36",
            "updated_at":"2019-01-17 20:39:36",
            "order_items":[
                {
                    "id":9,
                    "order_id":50,
                    "item_id":590,
                    "item_amount":1,
                    "item_name":"deserunt",
                    "item_price":6,
                    "created_at":"2019-01-17 20:39:37",
                    "updated_at":"2019-01-17 20:39:37"
                }
            ],
            "users":{
                "id":6,
                "name":"Blanca Schuster DDS",
                "email":"duane.watsica@example.com",
                "openid":"4WHOeUKHNt",
                "avatar":"iTRT2YAGni",
                "gender":0,
                "country":"YS16ISWMQU",
                "province":"mJWHFkbi2t",
                "city":"MiOqPu51aP",
                "email_verified_at":"2019-01-17 20:39:36",
                "created_at":"2019-01-17 20:39:36",
                "updated_at":"2019-01-17 20:39:36"
            }
        }
    ]
}

编写数据库迁移

外键需要在编写迁移时就定义好,请注意代码中的注释。

Users表

class CreateUsersTable extends Migration
{
    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->increments('id');
            $table->string('name');
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            $table->rememberToken();
            $table->timestamps();
        });
    }
}

Orders表

class CreateOrdersTable extends Migration
{
    public function up()
    {
        Schema::create('orders', function (Blueprint $table) {
            $table->increments('id');
            $table->string('order_sn')->comment('订单编号');
            //定义一个unsignedInteger类型的user_id键,这个键必须和users表中的主键类型一致。
            $table->unsignedInteger('user_id')->comment('用户id');
            //将user_id定义为外键,该外键指向的是users表中的id键。
            $table->foreign('user_id')->references('id')->on('users');
            $table->integer('shelf_id')->comment('货架号');
            $table->float('total_price')->comment('订单价格');
            $table->integer('pay_type')->comment('支付方式,0:微信')->default(0);
            $table->string('transaction_no')->comment('支付交易单号');
            $table->integer('status')->comment('订单状态')->default(0);
            $table->timestamps();
        });
    }
}

Products表

class CreateProductsTable extends Migration
{
    public function up()
    {
        Schema::create('products', function (Blueprint $table) {
            $table->increments('id');
            //定义一个unsignedInteger类型的order_id键,这个键必须和orders表的主键类型一致。
            $table->unsignedInteger('order_id')->comment('订单id');
            //将order_id定义为外键,该外键指向的是orders表中的id键。
            $table->foreign('order_id')->references('id')->on('orders');
            $table->unsignedInteger('item_id')->comment('产品id');
            $table->foreign('item_id')->references('id')->on('products');
            $table->integer('item_amount')->comment('产品数量');
            $table->string('item_name')->comment('产品名称');
            $table->float('item_price')->comment('产品单价');
            $table->timestamps();
        });
    }
}

创建Model并定义关系

Orders模型

class Orders extends Model
{
    public function users(){
        return $this->belongsTo(User::class,'user_id');
    }

    public function products(){
        return $this->hasMany(Products::class,'order_id');
    }
}

Products模型

class Products extends Model
{
    public function orders(){
        return $this->belongsTo(Orders::class);
    }
}

这部分代码表示order属于users,并且对应多个products;同时product属于orders。

查询

通过下面的语句即可查询某一用户的所有订单,及订单中的所有产品。

$products = Order::query()->where('user_id',1)->products;

foreach($products as $i){
    //
}

预加载(Eager Loading)

如果我们dd一下Order::query()->where('user_id',1)的结果,我们会发现其中并不包含products。因为这部分结果只有在products属性被访问时才会被查询。如果我们需要将order及products的数据在一次查询中获取,则需要使用预加载(Eager Loading)。使用如下语句即可。

$result = $orders->with('orderItems','users');

此外,在https://laravel.com/docs/5.7/eloquent-relationships#eager-loading 所述的场景中,每次循环中都需要进行一次查询来获得book的author。而使用预加载则可以将所有对author的查询在一次查询中完成。

如果需要实现嵌套预加载,例如通过orderItems中的product_id从product表中查询产品信息,使用如下语句即可。

$result = 4orders->with('orderItems.product','users');
w

参考资料

Show CommentsClose Comments

Leave a comment