Laravel 的 where or 查詢

chuoke發表於2019-11-17

我使用 Laravel 開發已經半年多,兩個專案。Laravel 完善的文件,以及強大的社群,幾乎可以應付所有疑問,這使我很少去看低層程式碼實現邏輯。儘管能很好的實現邏輯,但是對程式碼的把控還不是很好,就在前幾天,我需要寫一個帶有 OR 條件的查詢,當時讓我煞費苦心,我搜尋了很多資訊,都沒有查到,所有關於稍微複雜一點的 OR 查詢都是在講解這個匿名函式實現:


  DB::table('users')
   ->where('name', '=', 'John')
   ->where(function ($query) {
       $query->where('votes', '>', 100)
             ->orWhere('title', '=', 'Admin');
   })
   ->get();

而這並不能滿足我,也許是我的關鍵詞不對,沒有找到那個完美的答案,並且我想要更多更靈活的方式。

我要實現的 SQL 大概是這樣的:


 SELECT * FROM user
  WHERE 
    group_id = 'group id'
   AND (
     name = 'name'
     OR mobile_number = 'mobile number'
     OR email = 'email'
     OR `score` > 1000
   ) 

這是一類很常見的業務邏輯,可能你會覺得很簡單,我也知道怎麼去實現:


  DB::table('users')
     ->where('group_id', 'group id')
     ->where(function ($query) {
         $query->where('name', 'name')
               ->orWhere('mobile_number', '=', 'mobile number')
               ->orWhere('email', '=', 'email')
               ->orWhere('score', '>', '1000');
     })
     ->get();

但是實際的邏輯並不是這樣的,我還需要去判斷是否有對應的引數,才需要把對應查詢條件寫入,就像這樣:


  DB::table('users')
     ->where('group_id', 'group id')
     ->where(function ($query) {
        if ($params['name']) {
          $query->orWhere('name', $params['name'])
        }

        if ($params['mobile_number']) {
          $query->orWhere('mobile_number', $params['mobile_number'])
        }

        if ($params['email']) {
          $query->orWhere('email', $params['email'])
        }

        if ($params['score']) {
          $query->orWhere('score', '>', $params['score'])
        }

     })
     ->get();

我知道這可行,一直都是這樣寫的,但我覺得強大的 Laravel 肯定不會僅僅提供這種方式,於是我決定看一眼低層程式碼,很幸運,我有新的發現:


/**
 * Add a basic where clause to the query.
 *
 * @param  \Closure|string|array  $column
 * @param  mixed   $operator
 * @param  mixed   $value
 * @param  string  $boolean
 * @return $this
 */
public function where($column, $operator = null, $value = null, $boolean = 'and')
{
    // If the column is an array, we will assume it is an array of key-value pairs
    // and can add them each as a where clause. We will maintain the boolean we
    // received when the method was called and pass it into the nested where.
    if (is_array($column)) {
        return $this->addArrayOfWheres($column, $boolean);
    }

    // Rest of code ...
}

/**
 * Add an array of where clauses to the query.
 *
 * @param  array  $column
 * @param  string  $boolean
 * @param  string  $method
 * @return $this
 */
protected function addArrayOfWheres($column, $boolean, $method = 'where')
{
    return $this->whereNested(function ($query) use ($column, $method, $boolean) {
        foreach ($column as $key => $value) {
            if (is_numeric($key) && is_array($value)) {
                $query->{$method}(...array_values($value));
            } else {
                $query->$method($key, '=', $value, $boolean);
            }
        }
    }, $boolean);
}

where 方法中,我只保留了需要重點關注的一段程式碼,如果條件滿足,將會進入 addArrayOfWheres 方法,在這裡解析以陣列方式傳遞進來的條件引數,並且該組條件會被分組,也就是會用 () 包起來。有兩種方式可以讓查詢條件分組,一是陣列傳參,二是匿名函式。類似我這種 OR 條件的查詢,關鍵的就是讓查詢正確分組。

另外一個關鍵程式碼:


if (is_numeric($key) && is_array($value)) {
    $query->{$method}(...array_values($value));
}

陣列引數會被展開,看到這個,我想我的程式碼可以寫成這樣:


  $orWhere = [];
  if ($params['name']) {
      $orWhere[] = ['name', '=', $params['name'], 'OR'];
  }
  if ($params['mobile_number']) {
      $orWhere[] = ['mobile_number', '=', $params['mobile_number'], 'OR'];
  }
  if ($params['email']) {
      $orWhere[] = ['email', '=', $params['email'], 'OR'];
  }
  if ($params['score']) {
      $orWhere[] = ['score', '>', $params['score'], 'OR'];
  }

  DB::table('users')
     ->where('group_id', 'group id')
     ->where($orWhere)
     ->get();

$orWhere 會被分組且被 ...array_values($value) 展開。

也可以這樣:


  $orWhere = [];
  if ($params['name']) {
      $orWhere['name'] = $params['name'];
  }
  if ($params['mobile_number']) {
     $orWhere['mobile_number'] = $params['mobile_number'];
  }
  if ($params['email']) {
      $orWhere['email'] = $params['email'];
  }
  if ($params['score']) {
      $orWhere[] = ['score', '>', 1000, 'OR'];
  }

  DB::table('users')
     ->where('group_id', 'group id')
     ->where(function ($query) use ($orWhere) {
        $query->orWhere($orWhere);
     })
     ->get();

最終的 sql 是這樣的

select
  *
from
  `users`
where
  `group_id` = 'group id'
  and (
    (
      `name` = 'name'
      or `mobile_number` = 'mobile number'
      or `email` = 'email'
      OR `score` > 1000
    )
  )

雖然很多人都知道這個,我還是希望這能帶來些許啟發。

那麼還有沒有更多更靈活的方式呢?

初出茅廬,一知半解,望有識之士多多指教。抱拳...

相關文章