149

MySQL データベースのレコードの変更を追跡できるかどうか尋ねられました。そのため、フィールドが変更されると、古いものと新しいものとが利用可能になり、これが行われた日付が表示されます。これを行うための機能または一般的な手法はありますか?

もしそうなら、私はこのようなことを考えていました。というテーブルを作成しますchangesこれには、マスターテーブルと同じフィールドが含まれますが、古いものと新しいものをプレフィックスとして付けますが、実際に変更されたフィールドとそのフィールドのみが含まTIMESTAMPれます。で索引付けされますID。このようにして、SELECTレポートを実行して各レコードの履歴を表示できます。これは良い方法ですか?ありがとう!

4

9 に答える 9

217

これを行う簡単な方法を次に示します。

まず、追跡するデータ テーブルごとに履歴テーブルを作成します (以下のクエリの例)。このテーブルには、データ テーブルの各行に対して実行された各挿入、更新、および削除クエリのエントリがあります。

履歴テーブルの構造は、追跡するデータ テーブルと同じですが、次の 3 つの列が追加されています。発生した操作 (「アクション」と呼びましょう) を格納する列、操作の日時、および列です。シーケンス番号 (「リビジョン」) を格納します。これは操作ごとに増加し、データ テーブルの主キー列によってグループ化されます。

この順序付け動作を行うために、主キー列とリビジョン列に 2 列 (複合) インデックスが作成されます。履歴テーブルで使用されるエンジンが MyISAM である場合にのみ、この方法でシーケンスを実行できることに注意してください (このページの「MyISAM に関する注意事項」を参照してください)。

履歴テーブルの作成は非常に簡単です。以下の ALTER TABLE クエリ (およびその下のトリガー クエリ) で、「primary_key_column」をデータ テーブル内のその列の実際の名前に置き換えます。

CREATE TABLE MyDB.data_history LIKE MyDB.data;

ALTER TABLE MyDB.data_history MODIFY COLUMN primary_key_column int(11) NOT NULL, 
   DROP PRIMARY KEY, ENGINE = MyISAM, ADD action VARCHAR(8) DEFAULT 'insert' FIRST, 
   ADD revision INT(6) NOT NULL AUTO_INCREMENT AFTER action,
   ADD dt_datetime DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP AFTER revision,
   ADD PRIMARY KEY (primary_key_column, revision);

次に、トリガーを作成します。

DROP TRIGGER IF EXISTS MyDB.data__ai;
DROP TRIGGER IF EXISTS MyDB.data__au;
DROP TRIGGER IF EXISTS MyDB.data__bd;

CREATE TRIGGER MyDB.data__ai AFTER INSERT ON MyDB.data FOR EACH ROW
    INSERT INTO MyDB.data_history SELECT 'insert', NULL, NOW(), d.* 
    FROM MyDB.data AS d WHERE d.primary_key_column = NEW.primary_key_column;

CREATE TRIGGER MyDB.data__au AFTER UPDATE ON MyDB.data FOR EACH ROW
    INSERT INTO MyDB.data_history SELECT 'update', NULL, NOW(), d.*
    FROM MyDB.data AS d WHERE d.primary_key_column = NEW.primary_key_column;

CREATE TRIGGER MyDB.data__bd BEFORE DELETE ON MyDB.data FOR EACH ROW
    INSERT INTO MyDB.data_history SELECT 'delete', NULL, NOW(), d.* 
    FROM MyDB.data AS d WHERE d.primary_key_column = OLD.primary_key_column;

これで完了です。これで、'MyDb.data' 内のすべての挿入、更新、および削除が 'MyDb.data_history' に記録され、このような履歴テーブルが得られます (不自然な 'data_columns' 列を除く)。

ID    revision   action    data columns..
1     1         'insert'   ....          initial entry for row where ID = 1
1     2         'update'   ....          changes made to row where ID = 1
2     1         'insert'   ....          initial entry, ID = 2
3     1         'insert'   ....          initial entry, ID = 3 
1     3         'update'   ....          more changes made to row where ID = 1
3     2         'update'   ....          changes made to row where ID = 3
2     2         'delete'   ....          deletion of row where ID = 2 

更新から更新までの特定の列または列の変更を表示するには、主キー列とシーケンス列で履歴テーブルをそれ自体に結合する必要があります。この目的のために、次のようなビューを作成できます。

CREATE VIEW data_history_changes AS 
   SELECT t2.dt_datetime, t2.action, t1.primary_key_column as 'row id', 
   IF(t1.a_column = t2.a_column, t1.a_column, CONCAT(t1.a_column, " to ", t2.a_column)) as a_column
   FROM MyDB.data_history as t1 INNER join MyDB.data_history as t2 on t1.primary_key_column = t2.primary_key_column 
   WHERE (t1.revision = 1 AND t2.revision = 1) OR t2.revision = t1.revision+1
   ORDER BY t1.primary_key_column ASC, t2.revision ASC

編集:うわー、6年前の私の履歴テーブルが好きな人:P

私の実装はまだ順調に進んでおり、大きくなり、扱いにくくなっていると思います。このデータベースの履歴を参照するためのビューと非常に優れた UI を作成しましたが、これまであまり使用されていなかったと思います。だからそうなるのです。

順不同でいくつかのコメントに対処するには:

  • もう少し複雑な PHP で独自の実装を行い、コメントで説明されている問題のいくつかを回避しました (インデックスが大幅に転送されました。一意のインデックスを履歴テーブルに転送すると、問題が発生します。解決策があります。これはコメントで)。データベースがどの程度確立されているかによっては、この投稿をたどって手紙を読むのは冒険になるかもしれません.

  • 主キーとリビジョン列の関係がずれているように見える場合は、通常、複合キーが何らかの理由で壊れていることを意味します。まれに、これが発生し、原因がわかりませんでした。

  • このソリューションは、トリガーをそのまま使用して、かなりパフォーマンスが高いことがわかりました。また、MyISAM は、すべてのトリガーが実行する挿入が高速です。これは、スマート インデックス作成 (または ... の欠如) を使用してさらに改善できます。他の場所で重大な問題が発生していない限り、主キーを使用して MyISAM テーブルに単一の行を挿入することは、最適化する必要がある操作ではありません。私が MySQL データベースを実行している間ずっと、この履歴テーブルの実装がオンでしたが、発生した (多くの) パフォーマンスの問題のいずれの原因にもなりませんでした。

  • 挿入が繰り返される場合は、ソフトウェア層で INSERT IGNORE タイプのクエリを確認してください。うーん、今は思い出せませんが、複数の DML アクションを実行した後に最終的に失敗するこのスキームとトランザクションに問題があると思います。少なくとも知っておくべきことがあります。

  • 履歴テーブルとデータ テーブルのフィールドが一致していることが重要です。むしろ、データ テーブルには履歴テーブルよりも多くの列がありません。そうしないと、データ テーブルに対する挿入/更新/削除クエリが失敗し、履歴テーブルへの挿入によって (トリガー クエリの d.* が原因で) 存在しない列がクエリに挿入され、トリガーが失敗します。MySQL にスキーマ トリガーのようなものがあり、列がデータ テーブルに追加された場合に履歴テーブルを変更できるとしたら、すばらしいことです。MySQLは今それを持っていますか? 私は最近Reactをしています:P

于 2012-09-29T22:17:45.983 に答える
95

微妙です。

ビジネス要件が「データへの変更を監査したい - 誰がいつ何をしたか?」である場合、通常は監査テーブルを使用できます (Keethanjan が投稿したトリガーの例に従って)。私はトリガーの大ファンではありませんが、実装が比較的簡単であるという大きな利点があります。既存のコードは、トリガーや監査について知る必要はありません。

ビジネス要件が「過去の特定の日付のデータの状態を表示する」である場合、時間の経過に伴う変化の側面がソリューションに含まれていることを意味します。監査テーブルを見るだけでデータベースの状態をほぼ再構築できますが、それは難しく、エラーが発生しやすく、複雑なデータベース ロジックの場合は扱いにくくなります。たとえば、企業が「月の最初の日に未払いの未払いの請求書がある顧客に送信する必要がある手紙の住所を見つける」ことを知りたがっている場合、おそらく半ダースの監査テーブルをトロールする必要があります。

代わりに、時間の経過に伴う変化の概念をスキーマ設計に組み込むことができます (これは、Keethanjan が提案する 2 番目のオプションです)。これはアプリケーションの変更であり、間違いなくビジネス ロジックと永続化のレベルで行われるため、簡単なことではありません。

たとえば、次のようなテーブルがあるとします。

CUSTOMER
---------
CUSTOMER_ID PK
CUSTOMER_NAME
CUSTOMER_ADDRESS

経時的に追跡したい場合は、次のように修正します。

CUSTOMER
------------
CUSTOMER_ID            PK
CUSTOMER_VALID_FROM    PK
CUSTOMER_VALID_UNTIL   PK
CUSTOMER_STATUS
CUSTOMER_USER
CUSTOMER_NAME
CUSTOMER_ADDRESS

顧客レコードを変更するたびに、レコードを更新する代わりに、現在のレコードの VALID_UNTIL を NOW() に設定し、VALID_FROM (現在) と null の VALID_UNTIL を使用して新しいレコードを挿入します。「CUSTOMER_USER」ステータスを現在のユーザーのログイン ID に設定します (それを維持する必要がある場合)。顧客を削除する必要がある場合は、CUSTOMER_STATUS フラグを使用してこれを示します。このテーブルからレコードを削除することはできません。

そうすれば、特定の日付の顧客テーブルのステータス (住所は?) をいつでも確認できます。彼らは名前を変えましたか?同様の有効な日付と有効な日付を持つ他のテーブルに結合することで、全体像を歴史的に再構築できます。現在のステータスを確認するには、VALID_UNTIL 日付が null のレコードを検索します。

扱いにくいです (厳密に言えば、valid_from は必要ありませんが、クエリが少し簡単になります)。設計とデータベース アクセスが複雑になります。しかし、それは世界を再構築することをずっと簡単にします。

于 2012-09-24T11:45:46.663 に答える
17

これを解決するトリガーを作成できます。これを行うためのチュートリアルがあります(アーカイブされたリンク)。

データベースに制約とルールを設定することは、同じタスクを処理する特別なコードを記述するよりも優れています。これにより、別の開発者がすべての特別なコードをバイパスしてデータベースのデータ整合性を低下させる別のクエリを作成するのを防ぐことができるからです。

当時、MySQL はトリガーをサポートしていなかったので、長い間、スクリプトを使用して別のテーブルに情報をコピーしていました。このトリガーは、すべてを追跡するのにより効果的であることがわかりました.

このトリガーは、誰かが行を編集したときに変更された場合、古い値を履歴テーブルにコピーします。誰かがその行を編集するたびに元のテーブルに保存されますEditor IDlast mod時間は、現在の形に変更された時点に対応しています。

DROP TRIGGER IF EXISTS history_trigger $$

CREATE TRIGGER history_trigger
BEFORE UPDATE ON clients
    FOR EACH ROW
    BEGIN
        IF OLD.first_name != NEW.first_name
        THEN
                INSERT INTO history_clients
                    (
                        client_id    ,
                        col          ,
                        value        ,
                        user_id      ,
                        edit_time
                    )
                    VALUES
                    (
                        NEW.client_id,
                        'first_name',
                        NEW.first_name,
                        NEW.editor_id,
                        NEW.last_mod
                    );
        END IF;

        IF OLD.last_name != NEW.last_name
        THEN
                INSERT INTO history_clients
                    (
                        client_id    ,
                        col          ,
                        value        ,
                        user_id      ,
                        edit_time
                    )
                    VALUES
                    (
                        NEW.client_id,
                        'last_name',
                        NEW.last_name,
                        NEW.editor_id,
                        NEW.last_mod
                    );
        END IF;

    END;
$$

別の解決策は、リビジョン フィールドを保持し、保存時にこのフィールドを更新することです。max が最新のリビジョンであるか、0 が最新の行であると判断できます。それはあなた次第です。

于 2012-09-24T11:07:39.827 に答える
11

これが私たちがそれを解決した方法です

ユーザーテーブルは次のようになりました

Users
-------------------------------------------------
id | name | address | phone | email | created_on | updated_on

また、ビジネス要件が変化したため、ユーザーがこれまでに持っていたすべての住所と電話番号を確認する必要がありました。新しいスキーマは次のようになります

Users (the data that won't change over time)
-------------
id | name

UserData (the data that can change over time and needs to be tracked)
-------------------------------------------------
id | id_user | revision | city | address | phone | email | created_on
 1 |   1     |    0     | NY   | lake st | 9809  | @long | 2015-10-24 10:24:20
 2 |   1     |    2     | Tokyo| lake st | 9809  | @long | 2015-10-24 10:24:20
 3 |   1     |    3     | Sdny | lake st | 9809  | @long | 2015-10-24 10:24:20
 4 |   2     |    0     | Ankr | lake st | 9809  | @long | 2015-10-24 10:24:20
 5 |   2     |    1     | Lond | lake st | 9809  | @long | 2015-10-24 10:24:20

ユーザーの現在のアドレスを見つけるには、リビジョン DESC と LIMIT 1 で UserData を検索します。

特定の期間のユーザーのアドレスを取得するには、created_on bewteen (date1 , date 2) を使用できます。

于 2015-08-11T01:37:38.403 に答える
3

ちょうど私の2セント。トランジェントのソリューションと非常によく似た、変更内容を正確に記録するソリューションを作成します。

私のChangesTableは簡単です:

DateTime | WhoChanged | TableName | Action | ID |FieldName | OldValue

1) メインテーブルの行全体が変更されると、多くのエントリがこのテーブルに入りますが、その可能性は非常に低いため、大きな問題ではありません (人々は通常、1 つのことだけを変更します) 2) OldVaue (および、 want) は、任意のデータである可能性があるため、ある種の壮大な「anytype」である必要があります。これを RAW 型で行うか、JSON 文字列を使用して変換する方法があるかもしれません。

最小限のデータ使用量で、必要なものをすべて保存し、一度にすべてのテーブルに使用できます。今、自分で調べているのですが、このままではいけないかもしれません。

Create と Delete の場合は、行 ID のみで、フィールドは必要ありません。メインテーブルのフラグを削除すると(アクティブ?)、良いでしょう。

于 2016-07-15T07:46:53.297 に答える
0

これを行う直接的な方法は、テーブルにトリガーを作成することです。いくつかの条件またはマッピング方法を設定します。更新または削除が発生すると、「変更」テーブルに自動的に挿入されます。

しかし、最大の部分は、多数の列と多数のテーブルを取得した場合です。すべてのテーブルのすべての列の名前を入力する必要があります。明らかに、それは時間の無駄です。

これをより豪華に処理するために、列の名前を取得するためのプロシージャまたは関数をいくつか作成できます。

これを行うために、サードパーティ ツールを使用することもできます。ここでは、Java プログラム Mysql Trackerを作成します。

于 2016-03-04T05:26:40.127 に答える