使用Hashids取代increment ID

一直以來該使用increment IDuuid當作是資料庫主鍵一直是兩派爭論不休的議題。

increment ID的優點相當的顯而易見,使用上簡易方便並具有佔用空間小及順序性等優點,個人覺得比較容易注意到的缺點大概就是id容易被外人進行猜測,雖然說這個部分應該是系統開發者應注意的事,但總覺得increment ID這種顯示方式醜醜的(或應該說太漂亮?)

此外increment ID還有一個缺點就是當你有資料轉移或合併的需求時,increment ID會讓你很容易發生id重複的問題。

反觀uuid他最大的特點就是你光從id上來看無法看出任何資訊(例如:順序),他就長得像這樣: 05177f3c-bdf7-4b96-927d-f6f636175a27,通常他是128bit長的數字,並且用16進制表示。缺點是在很多情形下作為主鍵建立索引查詢效率低。

因此在我最近的一個專案中,我嘗試使用Hashids當作是我的替代方案,我資料表的主鍵依然是使用increment ID,但是在id暴露給外部時,他會經由Hashids幫你算成像GlaHquq0的結果,他是可以解密的,所以名字上雖然叫做Hashids,但本質和hash一點關係也沒有。

Hashids在各個程式語言都有實作library,根據官網上的實作原理,Hashids是一套將數字轉換為Hex結果的演算法,並且支援數字陣列及salt,以下為部分Hashids在javascript上的示意code。

function toHex(input) {

  var hash = "",
    alphabet = "0123456789abcdef",
    alphabetLength = alphabet.length;

  do {
    hash = alphabet[input % alphabetLength] + hash;
    input = parseInt(input / alphabetLength, 10);
  } while (input);

  return hash;

}

使用範例
Hashids的官方網站

在Laravel中實作也非常簡單,直接安裝hashids的package

composer require vinkla/hashids

我先將encode和decode寫成helper function,在其他地方要用到時能更方便呼叫

<?php

if (! function_exists('hash_encode')) {
    /**
     * Get result of Hashids encode
     */
    function hash_encode($value)
    {
        return \Hashids::encode($value);
    }
}

if (! function_exists('hash_decode')) {
    /**
     * Get result of Hashids decode
     */
    function hash_decode($value)
    {
        $result = \Hashids::decode($value);
        if (is_array($result) && count($result) === 1) {
            return $result[0];
        }

        return $result;
    }
}

另外我將Hashids寫成一個Trait便可以在需要用到的Eloquent Model中直接use

<?php

namespace Weding\CorePackage\Traits;

trait HashId
{
    public function getIdAttribute($value)
    {
        return $this->attributes['id'] = hash_encode($value);
    }

    public function getId()
    {
        return hash_decode($this->id);
    }
}

如此一來在Eloquent物件中id會自然被轉換成Hashids的結果,並能透過$this->getId()的方法來取得原先未演算過的increment ID