Erhalten alter Feldinhalte bis zum Speichern eines Datensatzes

cRM 11, Large

Hallo,
Ich benötige für ein paar Datenfelder eine Art ‚undo‘, also alle geänderten Datenfelder des DS belassen und nur einige, spezielle Felder wieder auf den Wert zurücksetzen, den sie beim letzten Speichern erhalten haben.
Realisiert habe ich das durch ‚verborgene‘ Felder in der DB-Tabelle, die im OnUpdate-Trigger mit den aktuellen Werten gefüllt werden. Dadurch habe ich immer die Werte seit der letzten Speicherung des Datensatzes zur Verfügung.

Nun habe ich eine Schaltfläche, die ein Skript ausführt, das nichts weiter tut, als sich auf das CurrentRecord zu setzen, die alten Werte per getContentsByName auszulesen und sie per

oCurrentRecord. Lock
oCurrentRecord. setContentsByName „feldname“
oCurrentRecord. Save
oCurrentRecord. Unlock

zurückzukopieren.
Allerdings wurde der DS vor erreichen des Lock bereits gespeichert, das heißt, der OnUpdate-Trigger hat die zwischengespeicherten Werte schon überschrieben.

Liege ich mit der Vermutung richtig, dass der DS bei der Zuweisung des CurrentRecord an die Variable gespeichert wird?
Wie lässt sich das von mir gewünschte Verhalten erzeugen?

Vielen Dank im Voraus
Wolfgang

Nein, ein Anfordern eines CurrentRecords speichert nichts implizit, automatisch.

Wenn Sie aktuell einen Datensatz bearbeiten und währenddessen diesen „Undo“ Button klicken, würde das Script gestartet und wenn in dem Script keine speziellen Vorkehrungen getroffen wurden (s.u.), dann würde jetzt erstmal die Frage kommen, ob man (vor der Scriptausführung) den Datensatz zunächst einmal speichern möchte. Wenn dies bejaht wird, haben Sie eine Speicherung „vor Lock“ (nämlich bevor das Script überhaupt losläuft).

Es gibt zwei Anweisungen, mit denen man im Script das o.a. Verhalten ändern kann:

<!--#pragma keepeditmode-->

Würde die o.a. Frage nach dem Speichern unterdrücken und einfach das Script direkt starten. Das wäre in Ihrem Fall aber schlecht, weil Sie ja im Script ja selber ebenfalls den Datensatz sperren, verändern und speichern. Das verträgt sich nicht. Das verträgt sich nur, wenn man mit CurrentInputForm(2) noch ein paar Felder in der Eingabemaske setzen (als hätte der:die Anwender:in es selbst eingetippt), aber die Hoheit über die Speicherung weiterhin dem Menschen überlassen will. Oder mit einem CurrentInputForm Wert etwas „ganz anderes“ machen möchte, aber eben den aktuellen Datensatz nicht über CurrentRecord verändert/speichert.

<!--#pragma autosave-->

Würde die o.a. Frage nach dem Speichern unterdrücken und den Datensatz einfach so wie er gerade ist abspeichern und dann das Script starten. Dies würde genau Ihrem Szenario entsprechen. Ist dieses #pragma in Ihrem Script?

Was mich etwas irritiert, ist, dass Sie von TRIGGER sprechen. Meinen Sie Datenbanktrigger? Sie dürften konzeptionell die „speziellen“ Felder überhaupt nur „sichern“, wenn sie überhaupt Teil der gerade gespeicherten Änderung sind. Sonst würde ja JEDES Speichern, auch wenn nur irgendein ganz anderes unbeteiligtes und irrelevante Feld kurz geändert worden wäre, egal durch wen oder was, sofort die Sicherung killen, wenn Sie einfach „stupide“ immer den aktuellen Inhalt der Spezialfelder bei jedem Speichern einfach in die korrespondierenden Backup-Felder umkopieren. Das darf nur passieren, wenn sie gerade auch nen neuen Inhalt kriegen/gekriegt haben. Das hat mit Ihrem Undo Script gar nichts zu tun. Verstehen Sie was ich meine?

Um dies zu realisieren, müssen Sie sich ein wenig in die Details der Trigger-Programmierung reinfuchsen.

Hier mal ein („as-is“ ungetestetes) Snippet als Vorlage (MSSQL) - für mehr fehlen uns im Rahmen des „normalen“ Supports leider die Ressourcen, da müssten wir dann doch irgendwie den Bogen zu unserem Dienstleistungsangebot geschlagen kriegen. :innocent:

Der Trigger „beobachtet“ 4 Felder (Street, ZIP, City und Country) in der Tabelle „Companies“ und würde nur die entscheidende Aktion (siehe „TODO“) nur lostreten, wenn eines dieser Felder geändert würde.

Hier klicken für MSSQL Trigger Rumpf-Beispiel (Nerd-Alert!)
ALTER TRIGGER [dbo].[cmbt_trigger_BackupAdressFields_Companies] ON [dbo].[Companies]
AFTER UPDATE
AS
DECLARE
	@ID				uniqueidentifier,
	@deletedCount	bigint,
	@insertedCount	bigint,
	@reason			int
	, @Street_New nvarchar(max), @Street_Old nvarchar(max), @ZIP_New nvarchar(max), @ZIP_Old nvarchar(max), @City_New nvarchar(max), @City_Old nvarchar(max), @Country_New nvarchar(max), @Country_Old nvarchar(max)
BEGIN

	SET NOCOUNT ON;

	-- check what kind of trigger was fired
	SELECT @deletedCount = COUNT(*) FROM deleted
	SELECT @insertedCount = COUNT(*) FROM inserted
	
	DECLARE
	@Street_Updated [bit]
	, @ZIP_Updated [bit]
	, @City_Updated [bit]
	, @Country_Updated [bit]

	IF UPDATE([Street])
		SET @Street_Updated = 1
	IF UPDATE([ZIP])
		SET @ZIP_Updated = 1
	IF UPDATE([City])
		SET @City_Updated = 1
	IF UPDATE([Country])
		SET @Country_Updated = 1

	-- means UPDATE
	IF @deletedCount = @insertedCount 
		SET @reason = 2
	
	-- means DELETED
	IF @insertedCount = 0 AND @deletedCount > 0
		SET @reason = 3
	
	-- means INSERT
	IF @deletedCount = 0 AND @insertedCount > 0
		SET @reason = 1
	
	IF (@reason = 2)
	BEGIN
		IF ((@Street_Updated = 1) OR (@ZIP_Updated = 1) OR (@City_Updated = 1) OR (@Country_Updated = 1))
		BEGIN
			-- create cursor for each record
			DECLARE record_cursor CURSOR LOCAL FAST_FORWARD FOR SELECT "ID" , "Street", "ZIP", "City", "Country" FROM inserted
	
			OPEN record_cursor 
			FETCH NEXT FROM record_cursor INTO @ID , @Street_New, @ZIP_New, @City_New, @Country_New
	
			WHILE @@FETCH_STATUS = 0 -- while FETCH statement is successful
			BEGIN
				IF ((@Street_Updated = 1) OR (@ZIP_Updated = 1) OR (@City_Updated = 1) OR (@Country_Updated = 1))
				BEGIN
					SELECT @Street_Old = Street, @ZIP_Old = ZIP, @City_Old = City, @Country_Old = Country FROM deleted WHERE ID = @ID
					IF ((@Street_New <> @Street_Old) OR (@ZIP_New <> @ZIP_Old) OR (@City_New <> @City_Old) OR (@Country_New <> @Country_Old))
					BEGIN 

					-- TODO: UPDATE corresponding backup-field(s) with ..._Old value(s) WHERE ID = @ID

					END
				END

				-- go to next
				FETCH NEXT FROM record_cursor INTO @ID , @Street_New, @ZIP_New, @City_New, @Country_New
			END

			CLOSE record_cursor
			DEALLOCATE record_cursor
		END
	END
END

Viel Erfolg! :four_leaf_clover:

Vielen Dank für die ausführliche Antwort!

  1. Das #pragma Autosave war wirklich noch im Skript!
  2. Ich habe wirklich einen Datenbanktrigger am Start, der (neben der „Zwischenspeicherung“) noch einen ganzen Haufen anderer Dinge tut, vielleicht hatte ich das in der Beschreibung unzulässig verkürzt…
  3. Der Ansatz mit CurrentInputForm(2) und dem <!--#pragma keepeditmode--> beschreibt genau das Verhalten, das ich haben möchte. Ich werde kommende Woche mal in diese Richtung weiterschauen.

Vielen Dank!
Wolfgang Doffek

1 „Gefällt mir“

Trigger haben so einige Tücken, können aber sehr nützlich sein. Zur Sache und zu dem Beispielcode habe ich drei Anmerkungen:

  1. Der Trigger feuert einmal pro UPDATE. Wenn das Update mehrere Datensätze verändert ist das dennoch ein Update, das ist klassisches Verhalten von MSSQL und unterscheidet sich in DBs wie z.B. MySQL. Hier wird dann der Inhalt von INSERTED (Tabelle mit geänderten bzw. neuen Datensätzen) mit Hilfe eines Cursors durchlaufen. Das kann man auch eleganter mit einem Statement lösen, siehe Beispiel.

  2. Man sollte sich immer darüber im klaren sein welche Spalten „zusammen gehören“. Im Fall einer Adresse oder einer Telefonnummer mit z.B. zwei Feldern (Vorwahl, Rufnummer) ist das überschaubar, auch hier würde ich sagen alles aus dem Datensatz gehört in den einen letzten Record. Aber man könnte auch auf die Idee kommen Spalten getrennt zu behandeln. Dann muss man eben höllisch aufpassen das beim Restore kein gemischter Datensatz entsteht.

  3. Die Funktion UPDATE() liefert auch True zurück wenn der Wert einfach nur neu gesetzt wird. Wenn ich also z.B. UPDATE tabelle SET spalte = 1 WHERE spalte = 1 auf die Tabelle ausführe ist UPDATE() immer Wahr. Das kann schon mal verwirren, ich vergleiche daher extra noch die Werte um nur bei tatsächlichen Änderungen tätig zu werden.

Hier ein Auszug aus meinen Log-Triggern die das Prinzip von 1) und 3) verdeutlichen. Hier wird jedes Feld einzeln behandelt:

CREATE TRIGGER	[dbo].[unt_bez_insert_log]
	ON			[dbo].[unt_bez]
	FOR INSERT, UPDATE, DELETE
AS

BEGIN
	SET NOCOUNT ON;

--[...]

	IF		UPDATE(bemerkung)
	BEGIN
		INSERT INTO unt_bez_log([...],benutzer,datum,spalte,aktion,neu,alt)
		SELECT	[...]
				( CASE WHEN i.pk IS NOT NULL AND d.pk IS NOT NULL THEN	'Update'
				  WHEN i.pk IS NOT NULL AND d.pk IS NULL THEN 'Insert' END ) AS aktion,
				left(i.bemerkung,300) AS neu,
				left(d.bemerkung,300) AS alt
		FROM	INSERTED i
		FULL JOIN DELETED d ON i.pk = d.pk
		WHERE	i.bemerkung IS NOT NULL
		AND		len(replace(i.bemerkung,' ','')) > 0
		AND		d.bemerkung IS NULL
		OR		d.bemerkung IS NOT NULL
		AND		len(replace(d.bemerkung,' ','')) > 0
		AND		i.bemerkung IS NULL
		OR		i.bemerkung != d.bemerkung
	END

--[...]