laravel 单库分表问题

关于laravel单库分表

laravel在使用关联关系的时候,对分表支持的并不友好。比如处理hasOne hasMany with等方法都不能使用,而且网上提供的解决方案,都是改造这几个方法,这就涉及到分表的业务代码也需要一定的更改。

我有个想法,在laravel最后生成sql语句的时候,发送到mysql执行前,进行拦截,并对其进行重写

举例

1.logs表是根据用户id分表的,表名为logs_1  logs_2
2.那么我业务逻辑中无需修改,按单表使用。只是查询的时候必须加上筛选条件user_id=xx

3.生成的语句是select * from logs where user_id=1;
4.此时只把user_id的值提取到表名上,转换为select * from logs_1; 再发送给mysql执行,那么就解决了laravel分表的问题

有点类似于简易版数据库中间件,仅仅是为了解决单库分表,laravel关联关系不好用的问题。
请问这样可行吗?
如果可行,在哪里监听,重写sql呢?
或者,你有啥优雅的方式解决单库分表吗?
ps:一些开源的数据库中间件mycat、dble等,对laravel支持的并不友好,比如不支持子查询等等,业务改造起来很麻烦。

———————————————-实践——————————————–
我发现通过设置模型提供的setTable()方法就可以了。with hasMany等方法都可以使用
1.使用静态方法,给模型设置分表标识
2.重写模型中的getTable()方法
3.使用with等方法调用分表的模型时,先用静态方法设置下表标识,代码如下

namespace App\Contracts\SubTable;


use Illuminate\Database\Eloquent\Model;

abstract class SubTableAbstract extends Model
{
    /**
     * 表名前缀
     */
    public static $prefixTable;

    /**
     * 分表标识
     */
    public static $identity;


    public function getTable()
    {
        return static::$prefixTable . static::$identity;
    }

    /**
     * @inheritdoc
     */
    public static function setTableIdentity(string $identity): void
    {
        static::$identity = $identity;
    }
}

要分表的模型继承上述父类,且在模型中设置分表前缀,如

public static $prefixTable = "crm_customers_";

使用时,需要先设置分表标识

//假设crms表与crm_customers_xxx是一对多的关系,模型名分别是Crm、CrmCustomer
$user_id='123456';
CrmCustomer::setTableIdentity($user_id);
Crm::query->with('crmCustomers')->first(); 

以上初步测试是可以的,不知是否有其他的未知问题? 欢迎评论

以下是查询的方式

  • 首先将需要查询的表的分表标识列出来
  • 然后拼接各个表查询的sql
  • 最后使用union 将各个sql拼接起来,作为一张临时表,这样就可以分页了
$queries = collect();
$userIds = [1,2,3,4,5]
foreach($userIds as $userId){
        $tableName = 'customers_' . $userId;
        $query = DB::table($tableName)
         ->select(['name', 'email', 'phone'])// 可以拼接一些查询
        // $query->where('phone','xxxx');
        // 加入集合
        queries->push($$query);
}
// 取出第一个作为查询对象,其他的作为合并对象
$unionQuery = $queries->shift();
$queries->each(function ($item) use ($unionQuery) {
      $unionQuery->unionAll($item);
});
// 设置临时表,执行查询
(new Customer)->setTable('union_customers')->from(DB::raw("({$unionQuery->toSql()}) as union_customers"))
->mergeBindings($unionQuery)
->paginate()

可以打印以下最终生成的sql

《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
最佳答案
<?php

namespace App\Scopes;

use Illuminate\Database\Eloquent\Scope;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;

class UserScope implements Scope
{
    public function apply(Builder $builder, Model $model)
    {
        $wheres = $builder->getQuery()->wheres;
        $userId = false;
        foreach ($wheres as $value) {
            if (isset($value['column']) && $value['column'] === 'user_id') {
                $userId = $value['value'];
            }
        }
        if ($userId !== false) {
            $builder->from("logs_{$userId}");
        }
        return $builder;
    }
}
3年前 评论
xiaobei 3年前
skphper 2年前
pndx 1年前
讨论数量: 22
<?php

namespace App\Scopes;

use Illuminate\Database\Eloquent\Scope;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;

class UserScope implements Scope
{
    public function apply(Builder $builder, Model $model)
    {
        $wheres = $builder->getQuery()->wheres;
        $userId = false;
        foreach ($wheres as $value) {
            if (isset($value['column']) && $value['column'] === 'user_id') {
                $userId = $value['value'];
            }
        }
        if ($userId !== false) {
            $builder->from("logs_{$userId}");
        }
        return $builder;
    }
}
3年前 评论
xiaobei 3年前
skphper 2年前
pndx 1年前
json991

期待大神的解决方案

3年前 评论

如果不用中间件 就写原生吧 或者先写关联获取sql替换表名称:joy:

3年前 评论
xiaopi

@putyy :joy: 我琢磨琢磨

3年前 评论

可以看看全局作用域,应该能解决你这个问题 快速入门《Laravel 8 中文文档》

3年前 评论
xiaopi (楼主) 3年前
  1. 写一个 middleware 拦截请求的 userId,用一个全局类来保存
  2. 重写 model 下的 getTable 方法,根据上面获取的 userId 来分表
3年前 评论
<?php

namespace App\Scopes;

use Illuminate\Database\Eloquent\Scope;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;

class UserScope implements Scope
{
    public function apply(Builder $builder, Model $model)
    {
        $wheres = $builder->getQuery()->wheres;
        $userId = false;
        foreach ($wheres as $value) {
            if (isset($value['column']) && $value['column'] === 'user_id') {
                $userId = $value['value'];
            }
        }
        if ($userId !== false) {
            $builder->from("logs_{$userId}");
        }
        return $builder;
    }
}
3年前 评论
xiaobei 3年前
skphper 2年前
pndx 1年前
xiaopi

@hello-bug 厉害了啊,兄弟。 :+1: 我测试测试

3年前 评论
hello-bug 3年前
xiaopi (作者) (楼主) 3年前
hello-bug 3年前
xiaopi (作者) (楼主) 3年前
hello-bug 3年前
xiaopi (作者) (楼主) 3年前
xiaopi

@Summer 哥有啥好方案吗

3年前 评论

mycat支持

2年前 评论

直接对表进行分区,不知道可行否

2年前 评论

那作为平台方 如何处理分表的数据统一在后台查询显示呢

1年前 评论
xiaopi (楼主) 1年前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!