Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

一意のIDのないデータベースのActiveRecord

時々、独特の状況や私たちの制御できないものが、非常に非正統的な要件につながることがあります。最近、レコードのデータベースIDに依存せずにActiveRecordを使用する必要があるという経験がありました。誰かが同じことを考えているなら、私は別の方法を見つけることを強くお勧めします!しかし、話の残りの部分に移りましょう。

決定がなされました。小さいデータベース(構造内のクローンであるがデータ内のクローンではない)をマージする必要がありました。チームがデータベースレコードをあるデータベースから別のデータベースにコピーして貼り付けるスクリプトに最後の仕上げをしているときに、私はプロジェクトに参加しました。 IDを含め、すべてをそのままコピーしました。

データベースA
id フルーツ user_id
... ... ...
123 オレンジ 456
... ... ...
データベースB
id フルーツ user_id
... ... ...
123 バナナ 74
... ... ...
マージ後のデータベースA
id フルーツ user_id
... ... ...
123 オレンジ 456
123 バナナ 74
... ... ...

これは、IDを持つ基本的な理由である一意の識別を破ります。詳細はわかりませんでしたが、IDが重複してシステムに導入されると、いろいろな問題が発生する気がしました。私は何かを言おうとしましたが、私はこのプロジェクトに不慣れであり、他の人たちはこれが最善の道であると確信しているようでした。数日後、コードをデプロイして、重複するIDを持つデータの処理を開始する予定でした。問題はもはや「これを行うべきか」ではありませんでした。代わりに、質問は「どうやってこれを行うのか」でした。と「これにはどれくらい時間がかかりますか?」

重複するIDの操作

では、IDが重複しているデータをどのように処理しますか?解決策は、いくつかのフィールドの複合IDを作成することでした。ほとんどのDBフェッチは次のようになりました:

# This doesn't work, there may be 2 users with id: 123
FavoriteFruit.find(123)

# Multiple IDs scope the query to the correct record
FavoriteFruit.find_by(id: 123, user_id: 456)

すべてのActiveRecord呼び出しはこのように更新され、コードを一瞥したとき、それは理にかなっているように見えました。展開するまで。

すべての地獄が解き放たれます

コードをデプロイした直後に、電話が鳴り始めました。顧客は、合計されなかった数を見ていました。彼らは自分の記録を更新することができませんでした。あらゆる種類の機能が壊れていました。

私たちは何をすべきか?コードをデプロイするだけではありません。また、あるデータベースから別のデータベースにデータを移動しました(また、デプロイ後に新しいデータが作成/更新されました)。それは単純なロールバック状況ではありませんでした。物事を迅速に修正する必要がありました。

Railsは何をしていますか?

デバッグの最初のステップは、現在の動作とエラーの再現方法を確認することでした。本番データのクローンを取り、Railsコンソールを起動しました。設定によっては、ActiveRecordクエリを実行したときにRailsが実行するSQLクエリが自動的に表示されない場合があります。 SQLステートメントがコンソールに表示されるようにする方法は次のとおりです。

ActiveRecord::Base.logger = Logger.new(STDOUT)

その後、いくつかの一般的なRailsクエリを試しました:

$ FavoriteFruit.find_by(id: 123, user_id: 456)

FavoriteFruit Load (0.6ms)
SELECT  "favorite_fruits".*
FROM "favorite_fruits"
WHERE "favorite_fruits"."id" = $1
AND "favorite_fruits"."user_id" = $2
[["id", "123"], ["user_id", "456"]]

find_by 正常に動作しているように見えましたが、次のようなコードが表示されました:

fruit = FavoriteFruit.find_by(id: 123, user_id: 456)
...
...
fruit.reload

そのreload 興味をそそられたので、それもテストしました:

$ fruit.reload

FavoriteFruit Load (0.3ms)
SELECT  "favorite_fruits".*
FROM "favorite_fruits"
WHERE "favorite_fruits"."id" = $1
LIMIT $2
[["id", 123], ["LIMIT", 1]]

ええとああ。したがって、最初はfind_byを使用して正しいレコードをフェッチしましたが 、reloadを呼び出すたびに 、レコードのIDを取得し、単純なIDによる検索クエリを実行します。もちろん、IDが重複しているため、誤ったデータが返されることがよくあります。

なぜそれをしたのですか?手がかりがないかRailsのソースコードを調べました。これはRubyonRailsを使用したコーディングの優れた側面であり、ソースコードはプレーンなRubyであり、自由にアクセスできます。 「ActiveRecordreload」をグーグルで検索したところ、すぐに見つかりました:

# File activerecord/lib/active_record/persistence.rb, line 602
def reload(options = nil)
  self.class.connection.clear_query_cache

  fresh_object =
    if options && options[:lock]
      self.class.unscoped { self.class.lock(options[:lock]).find(id) }
    else
      self.class.unscoped { self.class.find(id) }
    end

  @attributes = fresh_object.instance_variable_get("@attributes")
  @new_record = false
  self
end

これは、reloadであることを示しています は、多かれ少なかれ、self.class.find(id)のラッパーです。 。 IDのみによるクエリは、このメソッドに組み込まれていました。重複するIDを処理するには、コアRailsメソッドをオーバーライドするか(推奨されません)、reloadの使用を停止する必要があります。 完全に。

当社のソリューション

そのため、すべてのreloadを実行することにしました。 コード内でfind_byに変更します 複数のキーを介してデータベースをフェッチするため。

ただし、それは解決されたバグの一部にすぎませんでした。さらに掘り下げた後、updateをテストすることにしました。 呼び出し:

$ fruit = FavoriteFruit.find_by(id: 123, user_id: 456)
$ fruit.update(last_eaten: Time.now)

FavoriteFruit Update (43.3ms)
UPDATE "favorite_fruits"
SET "last_eaten" = $1
WHERE "favorite_fruits"."id" = $2
[["updated_at", "2020-04-16 06:24:57.989195"], ["id", 123]]

ええとああ。 find_byであっても、それを確認できます。 updateを呼び出したときに、特定のフィールドでレコードのスコープを設定しました Railsレコードに、単純なWHERE id = xを作成しました クエリ。これも重複したIDで壊れます。どうやってこれを回避したのですか?

カスタム更新メソッドupdate_uniqueを作成しました 、次のようになります:

class FavoriteFruit
  def update_unique(attributes)
    run_callbacks :save do
      self.class
        .where(id: id, user_id: user_id)
        .update_all(attributes)
    end
    self.class.find_by(id: id, user_id: user_id)
  end
end

これにより、IDを超えるスコープのレコードを更新できます:

$ fruit.update_unique(last_eaten: Time.now)

FavoriteFruit Update All (3.2ms)
UPDATE "favorite_fruits"
SET "last_eaten" = '2020-04-16 06:24:57.989195'
WHERE "favorite_fruits"."id" = $1
AND "favorite_fruits"."user_id" = $2
[["id", "123"], ["user_id", "456"]]

このコードは、レコードを更新するための狭い範囲を保証しましたが、クラスのupdate_allを呼び出すことによって メソッドでは、通常、レコードの更新に伴うコールバックが失われました。したがって、update_all以降、コールバックを手動で実行し、別のデータベース呼び出しを実行して、更新されたレコードを取得する必要がありました。 更新されたレコードを返しません。最終製品はあまりにもではありません 面倒ですが、fruit.updateよりも読みにくいことは間違いありません。 。

実際のソリューション

埋没費用、管理、および時間の制約のため、私たちのソリューションは、すべてのデータベース呼び出しに複数のキーを使用するようにRailsにモンキーパッチを適用することでした。これは、顧客がまだ製品を購入して使用するという意味で機能しましたが、いくつかの理由で悪い考えでした:

  • 将来の開発では、一般的なRailsメソッドを使用して誤ってバグを再導入する可能性があります。新しい開発者は、reloadを使用するなど、コードに隠れたバグがないようにするための厳格なトレーニングが必要になります。 メソッド。
  • コードはより複雑で、明確でなく、保守も困難です。これは、プロジェクトが進むにつれて開発速度をますます遅くする技術的負債です。
  • テストは大幅に遅くなります。関数が機能することだけでなく、さまざまなオブジェクトのIDが重複している場合にも機能することをテストする必要があります。テストの作成には時間がかかり、テストスイートを実行するたびに、追加のすべてのテストを実行するのに時間がかかります。プロジェクトの各開発者が考えられるすべてのシナリオを注意深くテストしないと、テストでバグを見逃しやすくなります。

この問題の本当の解決策は、そもそもIDが重複しないようにすることです。あるデータベースから別のデータベースにデータを転送する必要がある場合、それを実行するスクリプトはIDなしでデータを収集して挿入し、受信データベースが標準化された自動インクリメントカウンターを使用して、各レコードに独自の一意のIDを与えることができるようにする必要があります。

>

別の解決策は、すべてのレコードにUUIDを使用することです。このタイプのIDは、ランダムに作成された長い文字列です(整数IDのように段階的にカウントするのではありません)。そうすれば、データを他のデータベースに移動しても、競合や問題は発生しません。

つまり、Railsは、IDがレコードごとに一意であり、データベース内の特定のデータをすばやく簡単に操作する方法を理解して構築されたということです。 Railsは独創的なフレームワークであり、Railsのやり方に固執している限り、これの美しさはすべてがスムーズに実行されることです。これは、Railsだけでなく、プログラミングの他の多くの側面にも当てはまります。事態が複雑になった場合、問題を特定する方法を知っておく必要があります。ただし、明確で保守が容易な従来のコードを記述すれば、そもそもこれらの複雑さの多くを回避できます。


  1. リーダー向けの Microsoft Edge 独自の機能

    Microsoft Edge は、2015 年にリリースされた Windows の組み込み Web ブラウザーです。 Google Chrome が広く普及している Web ブラウザとして、Edge は追いつこうとしています。 今日は、Microsoft Edge で閲覧モードと組み込み辞書を使用する方法について説明します。 このアップデートは少し前に導入されましたが、ご存知ないかもしれません。ブラウジングには、人々が通常オンラインで読むのが好きなので、読むのに良い記事を検索することが含まれます. したがって、読書が好きなら、Chrome とは異なり、読書モードに拡張機能を必要としない

  2. Windows 7 を Windows 11 に無料でアップグレードする方法 (データ損失なし)

    まだ Windows 7 を使用している場合は、Microsoft が Windows 7 のサポートを終了したため、最新の Windows 11 にアップグレードする必要があります。これは、Windows を保護し、最近のバグを修正するために Microsoft が定期的にリリースするセキュリティ更新プログラムがデバイスに適用されていないことを意味します。 Windows 7 を Windows 11 に無料でアップグレードできます ?答えは「はい」ですが、まず、Windows 10 にアップグレードする必要があります。 次に、Windows 11を選択します.Microsoftは、互換性