最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

9個(gè)步驟帶你了解PHP如何安全地遷移數(shù)據(jù)庫(kù)或字段

 更新時(shí)間:2025年11月23日 08:40:47   作者:BingoGo  
這篇文章主要為大家詳細(xì)介紹了如何通過(guò)9個(gè)步驟了解PHP如何安全地遷移數(shù)據(jù)庫(kù)或字段,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下

問(wèn)題描述

這篇文章要講的是一個(gè)非常具體且棘手的問(wèn)題:唯一 ID 遷移。

現(xiàn)在有一個(gè)實(shí)體 User,由 User::$id 標(biāo)識(shí),看起來(lái)像這樣:

final class User
{
  public function __construct(
    public int $id,
  ) {}
}

訪(fǎng)問(wèn)它的數(shù)據(jù)的方式是通過(guò)一個(gè)名為 UserRepository 的倉(cāng)儲(chǔ)接口。這里提供一個(gè)簡(jiǎn)單的 SQLite 實(shí)現(xiàn):

interface UserRepository
{
  /**
   * @throws  UserNotFoundException
   */
  public function findById(
    int $id
  ): User;
}

final class SqliteUserRepository
  implements UserRepository
{
  public function findById(
    int $id
  ): User {
    $sql = "...";
    $stmt = $this->pdo->prepare($sql);
    $stmt->execute([
      'id' => $id,
    ]);
    // ...
  }
}

非常簡(jiǎn)單的設(shè)置。

現(xiàn)在假設(shè)你的團(tuán)隊(duì)決定,出于安全原因,用戶(hù)的唯一標(biāo)識(shí)符不應(yīng)再是整數(shù),而應(yīng)該采用 UUID。

當(dāng)然,不允許停機(jī)。

解決方案

為了絕對(duì)安全,將分三個(gè)階段實(shí)施:

  • 讓兩個(gè) ID 共存
  • 實(shí)戰(zhàn)測(cè)試 UUID 實(shí)現(xiàn)
  • 淘汰以前的整數(shù) ID 實(shí)現(xiàn)

分階段進(jìn)行的主要原因是,測(cè)試不足以確保一切都按預(yù)期工作。這個(gè)字段可能被其他作業(yè)通過(guò) API 使用,或者我現(xiàn)在甚至無(wú)法想象的東西。

所以以防萬(wàn)一,我希望能夠在任何時(shí)刻安全地回滾到以前的實(shí)現(xiàn)。

假設(shè)這里描述的每一步都有適當(dāng)?shù)臏y(cè)試覆蓋,理想情況下是在重構(gòu)發(fā)生之前。

步驟 1 - 從原始類(lèi)型解耦

無(wú)論你接下來(lái)做什么,沒(méi)有這個(gè)都不容易!User 類(lèi)中的那個(gè) int 原始類(lèi)型就是在要求爆炸發(fā)生。

如果你想從整數(shù)平滑過(guò)渡到 UUID,最好的辦法是首先將代碼與原始類(lèi)型解耦。一種方法是封裝你的原始類(lèi)型。我將創(chuàng)建一個(gè)名為 UserId 的類(lèi),讓代碼依賴(lài)它而不是 int

final class UserId
{
  public function __construct(
    public int $id,
  ) {}

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

final class User
{
  public function __construct(
    public UserId $id,
  ) {}
}

interface UserRepository
{
  /**
   * @throws  UserNotFoundException
   */
  public function findById(
    UserId $id
  ): User;
}

上面的代碼應(yīng)該會(huì)使重構(gòu)稍微容易一些。當(dāng)調(diào)用 getId() 時(shí),UserId 仍然返回 int,但這沒(méi)關(guān)系!最重要的是,我們的代碼依賴(lài)于 UserId——一個(gè)我們控制的類(lèi)型——而不是原始整數(shù)——我們根本無(wú)法控制它。

現(xiàn)在只需覆蓋所有現(xiàn)有代碼以使用 UserId 而不是 int $id。

final class SqliteUserRepository
  implements UserRepository
{
  public function findById(
    UserId $id
  ): User {
    $sql = "...";
    $stmt = $this->pdo->prepare($sql);
    $stmt->execute([
      'id' => $id->getId(),
    ]);
    // ...
  }
}

到這里,什么都沒(méi)有改變。感覺(jué)可以安全地合并和部署,不應(yīng)該有任何東西會(huì)崩潰。順便說(shuō)一句,測(cè)試幫助很大。一定要進(jìn)行測(cè)試!

步驟 2 - 讓兩個(gè)字段共存

現(xiàn)在確保可以向 users 表添加一個(gè)新字段。這樣就可以了:

sqlite> ALTER TABLE `users` ADD `uuid` VARCHAR;

現(xiàn)在它既不能是 NOT NULL 也不能是 UNIQUE,因?yàn)槊總€(gè)現(xiàn)有記錄的值都將是 NULL。

回到 UserId 類(lèi),確保它現(xiàn)在的實(shí)現(xiàn)中有 uuid

final class UserId
{
  public function __construct(
    public int $id,
    public ?UuidInterface $uuid,
  ) {}

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

  public function getUuid(): ?UUidInterface
  {
    return $this->uuid;
  }
}

它仍然是可空的,因?yàn)?,嗯,它在?shù)據(jù)庫(kù)中是 null!

現(xiàn)在需要確保發(fā)生兩件事:

  • 每個(gè)現(xiàn)有記錄都將有一個(gè)非空的 uuid;并且
  • 每個(gè)新記錄都將已經(jīng)帶有填充的 uuid

當(dāng)看到在任何給定時(shí)刻,users.uuid 永遠(yuǎn)不會(huì)是 NULL 時(shí),則認(rèn)為兩者在數(shù)據(jù)庫(kù)層都很好地共存。

步驟 3 - 確保每個(gè)新記錄都有 UUID

在你的系統(tǒng)中,某個(gè)地方存儲(chǔ)著 Users。需要確保在它發(fā)生的任何地方,UUID 字段都將被填充。

所以給定這個(gè)舊的實(shí)現(xiàn):

public function insert(
  User $user
): void {
  // insert into ...
}

只需用 UUID 生成來(lái)修補(bǔ)它,應(yīng)該就沒(méi)問(wèn)題了:

public function insert(
  User $user
): void {
  $id = $user->id;

  if ($id->uuid === null) {
    $id->uuid = Uuid::uuid4();
  }

  // insert into ...
}

個(gè)人強(qiáng)烈建議你用測(cè)試覆蓋這個(gè) IF 語(yǔ)句,以防你遺漏了導(dǎo)入或類(lèi)似的東西。除此之外,不應(yīng)該引入其他回歸。

每個(gè)新記錄現(xiàn)在應(yīng)該都有正確填充的 users.uuid。

步驟 4 - 為舊記錄回填 UUID 字段

這可以用腳本完成。如果你使用遷移框架,可能也會(huì)非常簡(jiǎn)單。

現(xiàn)在只需要獲取所有 uuid 為 null 的用戶(hù)并填充它們。類(lèi)似這樣就可以完成:

$users = getUsersWithEmptyUuid();
foreach ($users as $user) {
  $user->id->uuid = Uuid::uuid4();
  updateUser($user);
}

上面的代碼并不能代表每個(gè)代碼庫(kù),但我想你明白了。

步驟 5 - 確保一切正常運(yùn)行

不要急于切換實(shí)現(xiàn)。一定要確保系統(tǒng)正常運(yùn)行,并且在再運(yùn)行系統(tǒng)幾個(gè)小時(shí)后,users.uuid 不會(huì)是 NULL。

只有當(dāng)你 100% 確定 users.uuid 在此表中永遠(yuǎn)不會(huì)是 NULL 時(shí),才進(jìn)入下一步。

步驟 6 - 更新 UserRepository 以使用 UUID

看來(lái)現(xiàn)在已經(jīng)可以切換到新的 UUID 實(shí)現(xiàn)了。但不建議盲目地切換到新實(shí)現(xiàn)。

謹(jǐn)慎總比后悔好,對(duì)吧?首先確保用功能開(kāi)關(guān)保護(hù)代碼。用以下內(nèi)容更新 SqliteUserRepository

final class SqliteUserRepository
    implements UserRepository
{
  public function findById(UserId $id): User
  {
    if (
      isFeatureFlagActive('enableNewUsersUuidImplementation')
    ) {
      // 新實(shí)現(xiàn),使用 Uuid
      $sql = "...";
      $stmt = $this->pdo->prepare($sql);
      $stmt->execute([
        'uuid' => (string) $id->getUuid(),
      ]);
      // ...
    } else {
      // 舊實(shí)現(xiàn),使用整數(shù) $id
      $sql = "...";
      $stmt = $this->pdo->prepare($sql);
      $stmt->execute([
        'id' => $id->getId(),
      ]);
      // ...
    }
  }
}

長(zhǎng)話(huà)短說(shuō):如果請(qǐng)求的功能已啟用,isFeatureFlagActive() 返回 TRUE,否則返回 FALSE。它可以基于配置、數(shù)據(jù)庫(kù)條目或環(huán)境變量。這在這里不相關(guān)。

重要的是,你可以更改 isFeatureFlagActive() 的返回值,而無(wú)需重新部署代碼。這樣你就可以安全地回滾到以前的實(shí)現(xiàn),沒(méi)有太多摩擦。

步驟 7 - 部署、啟用和監(jiān)控

首先部署它,確保 isFeatureFlagActive() 始終返回 FALSE,這樣就會(huì)選擇原始實(shí)現(xiàn)。

然后將 isFeatureFlagActive() 切換為返回 TRUE,這樣就會(huì)選擇新實(shí)現(xiàn)——同樣,這可以通過(guò)數(shù)據(jù)庫(kù)記錄、環(huán)境變量、SaaS 工具或你喜歡的任何東西來(lái)完成。

哦不!出問(wèn)題了!網(wǎng)站突然變得超級(jí)慢!!

關(guān)閉你的功能開(kāi)關(guān),這樣 isFeatureFlagActive() 將再次返回 FALSE。

...

事情似乎又恢復(fù)正常了。回到你的 IDE,試著弄清楚發(fā)生了什么。也許做一些點(diǎn)擊測(cè)試和調(diào)試來(lái)理解是什么導(dǎo)致它如此緩慢。

最終你會(huì)意識(shí)到你沒(méi)有索引 users.uuid 列,所以由于你的巨大表,查詢(xún)它變得超級(jí)慢。盡快修復(fù)它!

步驟 8 - 使 UUID 唯一并建立索引

由于使用的是 SQLite 實(shí)現(xiàn),這里是應(yīng)該完成此操作的代碼片段:

sqlite> CREATE UNIQUE INDEX `users_uuid_uq` ON `users`(`uuid`);

理想情況下,你還應(yīng)該使 users.uuidNOT NULL,但我跳過(guò)了它,因?yàn)樗枰嗟?SQLite 步驟,這些步驟與我想在這里演示的內(nèi)容無(wú)關(guān)。

好了,現(xiàn)在應(yīng)該沒(méi)問(wèn)題了。將你的更改傳播到生產(chǎn)環(huán)境,看看功能開(kāi)關(guān)的代碼現(xiàn)在表現(xiàn)如何。

一切都好,對(duì)吧?是時(shí)候清理了。

步驟 9 - 清理你的數(shù)字 ID

既然東西已經(jīng)部署并經(jīng)過(guò)實(shí)戰(zhàn)測(cè)試,是時(shí)候清理以前的數(shù)字 id 字段了。

無(wú)論你是刪除實(shí)際字段還是只是不在代碼中使用它,這都是項(xiàng)目決策——什么不是呢?

但最終你的 SqliteUserRepository 會(huì)看起來(lái)像這樣:

final class SqliteUserRepository
    implements UserRepository
{
  public function findById(
    UserId $id
  ): User {
    $sql = "...";
    $stmt = $this->pdo->prepare($sql);
    $stmt->execute([
      'uuid' => (string) $id->getUuid(),
    ]);
    // ...
  }
}

插入記錄的函數(shù)現(xiàn)在也值得一些關(guān)愛(ài)。讓我們刪除以前的 IF 語(yǔ)句:

...

public function insert(
  User $user
): void {
  $user->id->uuid = Uuid::uuid4();

  // insert logic
}

...

如果你決定也從數(shù)據(jù)庫(kù)中刪除數(shù)字 id,必須確保 UserId 代碼也被清理,并刪除 $id 屬性:

final class UserId
{
  public function __construct(
    public UuidInterface $uuid,
  ) {}

  public function getUuid(
  ): UuidInterface {
    return $this->uuid;
  }
}

因?yàn)楝F(xiàn)在 UUID 完全沒(méi)有理由為空,也從 $uuid 屬性中刪除了問(wèn)號(hào)。現(xiàn)在你的系統(tǒng)是安全的!

總結(jié)

當(dāng)然,事情可能因項(xiàng)目而異,但歸根結(jié)底,你將執(zhí)行所描述技術(shù)的某種變體。

這適用于幾乎任何依賴(lài)數(shù)據(jù)的實(shí)現(xiàn)更改。只需記住三個(gè)階段:

  • 讓兩個(gè)實(shí)現(xiàn)共存
  • 實(shí)戰(zhàn)測(cè)試新實(shí)現(xiàn)
  • 淘汰以前的實(shí)現(xiàn)

不要害羞或羞于采取多個(gè)步驟。即使你知道之后必須刪除代碼!實(shí)際上回滾部署或修復(fù)實(shí)時(shí)數(shù)據(jù)庫(kù)比在這里描述的任何步驟都要痛苦得多。

到此這篇關(guān)于9個(gè)步驟帶你了解PHP如何安全地遷移數(shù)據(jù)庫(kù)或字段的文章就介紹到這了,更多相關(guān)PHP遷移數(shù)據(jù)庫(kù)或字段內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評(píng)論

高安市| 太仓市| 都匀市| 蛟河市| 焦作市| 常山县| 杭锦后旗| 磐安县| 古田县| 浮梁县| 沂南县| 海林市| 梁山县| 两当县| 保山市| 民县| 陆川县| 汪清县| 五台县| 苗栗县| 贞丰县| 吉安市| 万荣县| 武胜县| 盐边县| 龙南县| 富蕴县| 松原市| 恩平市| 锦州市| 曲水县| 徐水县| 临桂县| 额尔古纳市| 威信县| 吴江市| 老河口市| 衡水市| 溧阳市| 九龙城区| 怀化市|