track change in sql row for update - mysql

I've a java program to update a SQL table (id, name, status). Entire table is updated with same data or some changed data. How can I track if a row is same like it was before update or it has some modified data? id will be always same, only small typo on name. I just want to check on next update if name is modified. In this case the status field should be changed from 'same' to 'modified'. Will timestamp solve my issue? Please help.

1 - If you are looking to audit the table (inserts, updates, deletes), look at my how to prevent unwanted transactions slide deck w/code - http://craftydba.com/?page_id=880.
SEE CODE AT END!
The trigger that fills the audit table can hold information from multiple tables since the data is saved as XML. Therefore, you can un-delete if necessary. It tracks who and what made the change.
2 - If you are never going to purge the data from the audit table, why not mark the row as deleted but keep it for ever?
Many systems like people soft use effective dating to show if a record is no longer active. In the BI world this is called a type 2 dimensional table (slowly changing dimensions).
See the data warehouse institute article. http://www.bidw.org/datawarehousing/scd-type-2/
Each record has a begin and end date. All active records have a end date of null.
3 - Micorsoft SQL Server introduced the change data capture feature. While this tracks data change using a LOG reader after the fact, it lacks things like who and what made the change.
Again, all the above solutions work. I am partial to my solution!
Sincerely
John
The Crafty DBA
--
-- 7 - Auditing data changes (table for DML trigger)
--
-- Delete existing table
IF OBJECT_ID('[AUDIT].[LOG_TABLE_CHANGES]') IS NOT NULL
DROP TABLE [AUDIT].[LOG_TABLE_CHANGES]
GO
-- Add the table
CREATE TABLE [AUDIT].[LOG_TABLE_CHANGES]
(
[CHG_ID] [numeric](18, 0) IDENTITY(1,1) NOT NULL,
[CHG_DATE] [datetime] NOT NULL,
[CHG_TYPE] [varchar](20) NOT NULL,
[CHG_BY] [nvarchar](256) NOT NULL,
[APP_NAME] [nvarchar](128) NOT NULL,
[HOST_NAME] [nvarchar](128) NOT NULL,
[SCHEMA_NAME] [sysname] NOT NULL,
[OBJECT_NAME] [sysname] NOT NULL,
[XML_RECSET] [xml] NULL,
CONSTRAINT [PK_LTC_CHG_ID] PRIMARY KEY CLUSTERED ([CHG_ID] ASC)
) ON [PRIMARY]
GO
-- Add defaults for key information
ALTER TABLE [AUDIT].[LOG_TABLE_CHANGES] ADD CONSTRAINT [DF_LTC_CHG_DATE] DEFAULT (getdate()) FOR [CHG_DATE];
ALTER TABLE [AUDIT].[LOG_TABLE_CHANGES] ADD CONSTRAINT [DF_LTC_CHG_TYPE] DEFAULT ('') FOR [CHG_TYPE];
ALTER TABLE [AUDIT].[LOG_TABLE_CHANGES] ADD CONSTRAINT [DF_LTC_CHG_BY] DEFAULT (coalesce(suser_sname(),'?')) FOR [CHG_BY];
ALTER TABLE [AUDIT].[LOG_TABLE_CHANGES] ADD CONSTRAINT [DF_LTC_APP_NAME] DEFAULT (coalesce(app_name(),'?')) FOR [APP_NAME];
ALTER TABLE [AUDIT].[LOG_TABLE_CHANGES] ADD CONSTRAINT [DF_LTC_HOST_NAME] DEFAULT (coalesce(host_name(),'?')) FOR [HOST_NAME];
GO
--
-- 8 - Make DML trigger to capture changes
--
-- Delete existing trigger
IF OBJECT_ID('[ACTIVE].[TRG_FLUID_DATA]') IS NOT NULL
DROP TRIGGER [ACTIVE].[TRG_FLUID_DATA]
GO
-- Add trigger to log all changes
CREATE TRIGGER [ACTIVE].[TRG_FLUID_DATA] ON [ACTIVE].[CARS_BY_COUNTRY]
FOR INSERT, UPDATE, DELETE AS
BEGIN
-- Detect inserts
IF EXISTS (select * from inserted) AND NOT EXISTS (select * from deleted)
BEGIN
INSERT [AUDIT].[LOG_TABLE_CHANGES] ([CHG_TYPE], [SCHEMA_NAME], [OBJECT_NAME], [XML_RECSET])
SELECT 'INSERT', '[ACTIVE]', '[CARS_BY_COUNTRY]', (SELECT * FROM inserted as Record for xml auto, elements , root('RecordSet'), type)
RETURN;
END
-- Detect deletes
IF EXISTS (select * from deleted) AND NOT EXISTS (select * from inserted)
BEGIN
INSERT [AUDIT].[LOG_TABLE_CHANGES] ([CHG_TYPE], [SCHEMA_NAME], [OBJECT_NAME], [XML_RECSET])
SELECT 'DELETE', '[ACTIVE]', '[CARS_BY_COUNTRY]', (SELECT * FROM deleted as Record for xml auto, elements , root('RecordSet'), type)
RETURN;
END
-- Update inserts
IF EXISTS (select * from inserted) AND EXISTS (select * from deleted)
BEGIN
INSERT [AUDIT].[LOG_TABLE_CHANGES] ([CHG_TYPE], [SCHEMA_NAME], [OBJECT_NAME], [XML_RECSET])
SELECT 'UPDATE', '[ACTIVE]', '[CARS_BY_COUNTRY]', (SELECT * FROM deleted as Record for xml auto, elements , root('RecordSet'), type)
RETURN;
END
END;
GO

SQL Server doesn't have versioning on the table level. If you want to track difference between two field you have at least two options:
Control from your Java application - do pre-update check in your update method.
Control from SQL Server - write trigger again for pre-update check
You can also create suplimentary field where Version number will be kept

Yes you can use time stamp.
By using time stamp, you can find out the latest entry into the table and by using Order by timestamp in the query,you can get the latest and set the status based on corresponding value

It depends on how much information you need. If all you care about is whether the record has ever been modified, you can use created_when and updated_when fields. If the latter is greater than the former, it's been updated.
If you want to know what fields have been updated, you have to log the changes. The details depend on your requirements. If you need to log changes, a trigger is the best way to do it.

Related

Insert only when auto-increment id is not equal 6(for example)?

I have a table with 3 fields: Id(PK,AI), Name(varchar(36)), LName(varchar(36)).
I have to insert name and last name, Id inserts automatically because of it's constraints,
Is There a way to Jump id auto increment value when it reaches 6?
for instance do this 7 times:
Insert Into table(Name, LName) Values ('name1', 'lname1') "And jump id to 7 if it is going to be 6"
It may sound stupid to do this but I have the doubt.
Also Jump and do not record id 6.
record only, 1-5, 7,8,9 and so on
What I want to achieve starts from a Union:
Select * From TableNames
Union All
Select * From TableNames_general
In the TableNames_general I assign it's first value so that when the user sees the table for the first time it will be displayed the record I inserted.
The problem comes when the user inserts a new record, if the Id of the inserted record is the same as the one I have inserted it will be duplicated, that is why I want to achieve when the users inserts one record and if the last insert id already exists just jump that record. this is because I must have different ids due to its relationship among child tables.
Identity column generate values for you, And its best left this way, You have the ability to insert specific values in Identity column but its best left alone and let it generate values for you.
Imagine you have inserted a value explicitly in an identity column and then later on Identity column generates the same value for you, you will end up with duplicates.
If you want to have your input in that column then why bother with identity column anyway ??
Well this is not the best practice but you can jump to a specific number by doing as follows:
MS SQL SERVER 2005 and Later
-- Create test table
CREATE TABLE ID_TEST(ID INT IDENTITY(1,1), VALUE INT)
GO
-- Insert values
INSERT INTO ID_TEST (VALUE) VALUES
(1),(2),(3)
GO
-- Set idnentity insert on to insert values explicitly in identity column
SET IDENTITY_INSERT ID_TEST ON;
INSERT INTO ID_TEST (ID, VALUE) VALUES
(6, 6),(8,8),(9,9)
GO
-- Set identity insert off
SET IDENTITY_INSERT ID_TEST OFF;
GO
-- 1st reseed the value of identity column to any smallest value in your table
-- below I reseeded it to 0
DBCC CHECKIDENT ('ID_TEST', RESEED, 0);
-- execute the same commad without any seed value it will reset it to the
-- next highest idnetity value
DBCC CHECKIDENT ('ID_TEST', RESEED);
GO
-- final insert
INSERT INTO ID_TEST (VALUE) VALUES
(10)
GO
-- now select data from table and see the gap
SELECT * FROM ID_TEST
If you query the database to get the last inserted ID, then you can check if you need to increment it, by using a parameter in the query to set the correct ID.
If you use MSSQL, you can do the following:
Before you insert check for the current ID, if it's 5, then do the following:
Set IDENTITY_INSERT to ON
Insert your data with ID = 7
Set IDENTITY_INSERT to OFF
Also you might get away with the following scenario:
check for current ID
if it's 5, run DBCC CHECKIDENT (Table, reseed, 6), it will reseed the table and in this case your next identity will be 7
If you're checking for current identity just after INSERT, you can use SELECT ##IDENTITY or SELECT SCOPE_IDENTITY() for better results (as rcdmk pointed out in comments)
Otherwise you can just use select: SELECT MAX(Id) FROM Table
There's no direct way to influence the AUTO_INCREMENT to "skip" a particular value, or values on a particular condition.
I think you'd have to handle this in an AFTER INSERT trigger. An AFTER INSERT trigger can't update the values of the row that was just inserted, and I don't think it can make any modifications to the table affected by the statement that fired the trigger.
A BEFORE INSERT trigger won't work either, because the value assigned to an AUTO_INCREMENT column is not available in a BEFORE INSERT trigger.
I don't believe there's a way to get SQL Server IDENTITY to "skip" a particular value either.
UPDATE
If you need "unique" id values between two tables, there's a rather ugly workaround with MySQL: roll your own auto_increment behavior using triggers and a separate table. Rather than defining your tables with AUTO_INCREMENT attribute, use a BEFORE INSERT trigger to obtain a value.
If an id value is supplied, and it's larger than the current maximum value from the auto_increment column in the dummy auto_increment_seq table, we'd need to either update that row, or insert a new one.
As a rough outline:
CREATE TABLE auto_increment_seq
(id INT NOT NULL PRIMARY KEY AUTO_INCREMENT) ENGINE=MyISAM;
DELIMITER $$
CREATE TRIGGER TableNames_bi
BEFORE INSERT ON TableNames
FOR EACH ROW
BEGIN
DECLARE li_new_id INT UNSIGNED;
IF ( NEW.id = 0 OR NEW.id IS NULL ) THEN
INSERT INTO auto_increment_seq (id) VALUES (NULL);
SELECT LAST_INSERT_ID() INTO li_new_id;
SET NEW.id = li_new_id;
ELSE
SELECT MAX(id) INTO li_max_seq FROM auto_increment_seq;
IF ( NEW.id > li_max_seq ) THEN
INSERT INTO auto_increment_seq (id) VALUES (NEW.id);
END IF;
END IF;
END$$
CREATE TRIGGER TableNames_ai
AFTER INSERT ON TableNames
FOR EACH ROW BEGIN
DECLARE li_max_seq INT UNSIGNED;
SELECT MAX(id) INTO li_max_seq FROM auto_increment_seq;
IF ( NEW.id > li_max_seq ) THEN
INSERT INTO auto_increment_seq (id) VALUES (NEW.id);
END IF;
END;
DELIMITER ;
The id column in the table could be defined something like this:
TableNames
( id INT UNSIGNED NOT NULL DEFAULT 0 PRIMARY KEY
COMMENT 'populated from auto_increment_seq.id'
, ...
You could create an identical trigger for the other table as well, so the two tables are effectively sharing the same auto_increment sequence. (With less efficiency and concurrency than an Oracle SEQUENCE object would provide.)
IMPORTANT NOTES
This doesn't really insure that the id values between the tables are actually kept unique. That would really require a query of the other table to see if the id value exists or not; and if running with InnoDB engine, in the context of some transaction isolation levels, we might be querying a stale (as in, consistent from the point in time at the start of the transaction) version of the other table.
And absent some additional (concurrency killing) locking, the approach outline above is subject to a small window of opportunity for a "race" condition with concurrent inserts... the SELECT MAX() from the dummy seq table, followed by the INSERT, allows a small window for another transaction to also run a SELECT MAX(), and return the same value. The best we can hope for (I think) is for an error to be thrown due to a duplicate key exception.
This approach requires the dummy "seq" table to use the MyISAM engine, so we can get an Oracle-like AUTONOMOUS TRANSACTION behavior; if inserts to the real tables are performed in the context of a REPEATABLE READ or SERIALIZABLE transaction isolation level, reads of the MAX(id) from the seq table would be consistent from the snapshot at the beginning of the transaction, we wouldn't get the newly inserted (or updated) values.
We'd also really need to consider the edge case of an UPDATE of row changing the id value; to handle that case, we'd need BEFORE/AFTER UPDATE triggers as well.

Inserted rows vanish

We're having a problem on a wiki site (using mysql 5.5.9). There is a text table, and a revision table where revision.rev_text_id is a foreign key to text.old_id (there isn't a new one, ask wikimedia).
There is an INSERT INTO text..., then INSERT INTO revision... which uses the new old_id/rev_text_id received from the previous query.
The second query works, the first - I'm not sure. The thing is after the whole thing is gone through (with a few dozen more queries) the revision row is there, with a fresh new value in it's rev_text_id column.
However, the text row isn't there. Funny thing - the text table auto increment advances, as the next action skips a value for it's old_id, the missing value matching what we have in revision table.
Is there a chance the first query makes the auto increment index advance though the row isn't actually inserted? What causes this?
ADDTITION
When I copied the INSERT INTO text query from the logs and ran it on the server - it executed fine (row was added to table)
EDIT
Full queries are:
INSERT INTO text (old_id,old_text,old_flags) VALUES (NULL,'{text input by user}','utf8');
INSERT INTO `revision` (rev_id,rev_page,rev_text_id,rev_comment,rev_minor_edit,rev_user,rev_user_text,rev_timestamp,rev_deleted,rev_len,rev_parent_id,rev_sha1) VALUES (NULL,'{pageId}','{textId}','{comment}','{isMinor}','{userId}','{userName}','{TS}','{isDeleted}','{length}','{parentRevision}','{HASH}')"
Also, if relevant, text table is running InnoDB and revision is MyISAM.
EDIT
More information from logs:
Bad request
BEGIN
INSERT INTO `text`
INSERT INTO `revision`
UPDATE `page` SET page_latest
INSERT INTO `recentchanges`
INSERT INTO `cu_changes`
SELECT wl_user FROM `watchlist`
SELECT user_id FROM `user`
SELECT user_id,user_name,user_real_name,user_password,user_newpassword,user_newpass_time,user_email,user_touched,use
SELECT ug_group FROM `user_groups`
SELECT up_property,up_value FROM `user_properties`
SELECT user_id,user_name,user_real_name,user_password,user_newpassword,user_newpass_time,user_email,user_touched,use
SELECT ug_group FROM `user_groups`
SELECT up_property,up_value FROM `user_properties`
SELECT lc_value FROM `l10n_cache`
SELECT lc_value FROM `l10n_cache`
...
A few more SELECTs. Pause of activity for 2 sec, then the log has commands from a new user, no more queries from this user (on same thread).
An edit by same user to a different page, which went well:
BEGIN
INSERT INTO `text`
INSERT INTO `revision`
UPDATE `page` SET page_latest
INSERT INTO `recentchanges`
INSERT INTO `cu_changes`
SELECT wl_user FROM `watchlist`
COMMIT
BEGIN
UPDATE `watchlist` SET wl_notificationtimestamp
COMMIT
BEGIN
SELECT user_id FROM `user`
SELECT user_id,user_name,user_real_name,user_password,user_newpasswo
SELECT ug_group FROM `user_groups`
SELECT up_property,up_value FROM `user_properties`
SELECT * FROM `user`
SELECT up_property,up_value FROM `user_properties`
INSERT INTO `logging`
UPDATE `user` SET user_editcount=user_editcount+1
SELECT 1 FROM `user`
UPDATE `user` SET user_touched = '20121227211743'
COMMIT
etc.
Is there a chance the first query makes the auto increment index advance though the row isn't actually inserted? What causes this?
Yes, when you try to insert a row in a table with an auto_increment column, the insertion will fail, but the auto_increment value will be incremented by 1.
If you decide to convert the tables to InnoDB, I suggest you to use transactions. This way if one the queries fails none of them will be inserted.

how to force insert statement to run without inserting any records in values clause

i have an situation where i have a following table.
create table Bulk_Data
(
id int identity(1,1),
UserName varchar(100) not null default 'default value ',
iPartitionID int null
)
also i have an Insert trigger on Bulk_Data.
create trigger dbo.Bulk_Data_IU on Bulk_Data
AFTER INSERT
AS Begin
Merge Bulk_Data p1
using Bulk_Data p2 on p1.id = p2.id
when matched then
update set p1.iPartitionID = right(p2.id,1);
end
condition in above table is like
i have 3 column which are not depended on any values.
1] id is identity auto increment column
2] UserName is set to be default values
3] iPartitionID is based on insert in Trigger.
so my question is how should i insert the records say suppose i do not required
to insert any values in #2 i.e. in column 2 so how should i fire the insert command on table.
because insert command is important for me as i have created Insert trigger.
when i run
insert Bulk_Data(UserName) values('Suraj Sheikh')
it works fine but what if i don't want to insert any UserName.
is this possible ?
please help me out here.
You can use a computed column instead of a trigger that updates all rows for each insert.
create table Bulk_Data
(
id int identity(1,1),
UserName varchar(100) not null default 'default value ',
iPartitionID as id % 10 persisted
)
Use "Instead OF" trigger rather than using "AFTER INSERT" trigger.
INSTEAD-OF triggers are very powerful objects in SQL Server. They allow the developer to divert the database engine to do something different than what the user is trying to do.
In this way, when an insert query is fired on that table, insertion will not happen and whatever statements are written in trigger will actually applied to db.
Limitation - INSTEAD OF DELETE/UPDATE triggers cannot be defined on a table that has a foreign key with a cascade on DELETE/UPDATE action defined but Insert Trigger can be defined.

In mySQL, how do I make a Trigger so that one table keeps a change log from ALL other tables?

My idea was to have a table called "changelog_table" that had the following columns:
updated_table //the table being updated
updated_column //the column being updated
updated_row //the id of the row being updated
updated_content //this is what they updated the field to
updated_user //the user who updated
updated_datetime //the timestamp it was updated
I think this is both the minimum and maximum of what I'd really want, but I may be wrong.
Also...I do not understand, after weeks of reading, how to store variables (like "which table is being updated" and "which column is being updated" and so forth) in my trigger.
So let's say I had a table called "foo_table", with a column "bar_column", with a row "58008", that is being updated to "this is the new content", by user "peter_griffin", at 12/30/2013 at noon.
What would a trigger that could capture that look like?
You'll need to create separate triggers on each table for UPDATE (and, if so desired, for INSERT and DELETE too). They could each call the same stored procedure that does the actual logging.
The trigger can pass to the stored procedure a parameter containing the name of the table on which the operation is being performed (since the trigger is table-specific, it will know this - it'll have to be hardcoded); to detect which column(s) have been updated you'll need to compare, within each trigger, NEW.column with OLD.column for each column in the respective table.
For example:
CREATE TRIGGER upd_tbl_name AFTER UPDATE ON tbl_name FOR EACH ROW
CALL AuditLog('UPDATE', 'tbl_name', NEW.id, CONCAT_WS(',',
IF(NEW.columnA <=> OLD.columnA, NULL, CONCAT('columnA:=', NEW.columnA)),
IF(NEW.columnB <=> OLD.columnB, NULL, CONCAT('columnB:=', NEW.columnB)),
-- etc.
));
CREATE PROCEDURE AuditLog(
Action VARCHAR(10),
TableName VARCHAR(64),
RowID INT,
Columns TEXT,
)
INSERT INTO changelog_table VALUES (
Action,
TableName,
RowID,
Columns
USER(),
NOW(),
);

How to assign a foreign key value using a a before insert trigger

I have a scenario like this:
There are two tables table1 and table2. The table1 has a primary key pkey and table2 has a foreign key fkey now during an insert if the foreign key is provided the value should be inserted as it is. Otherwise, it has to get a primary key from table1 using some computation and determine the foreign key to be inserted. How do i do this??
I am using MySql 5.0
EDIT
In my scenario, table1 holds the billing details, that is, the table1 has the bills and the total amount to be paid. The customer pays some amount of the total outstanding balance or will pay for a particular bill. What i want to do here is. When i am not provided with a bill_id (which is primary key in table1 and foreign key in table2) i would like to search for the oldest bill that is due in table1 and deduct the amount due and further deduct the remaining amount if any from the next bill in the billed order. I want to do this in the database layer rather than the upper layer. So when an insert is being done without a value for the foreign key the value should be retrieved and placed by the trigger or otherwise directly inserted. How do i achieve this?
Using the answers provided here, i tried this:
CREATE DEFINER=`root`#`localhost` TRIGGER `inflow_pay_done_insert` BEFORE INSERT ON `inflow_pay_done` FOR EACH ROW BEGIN
DECLARE pkey INT;
SET pkey = (SELECT bill_id from inflow_bills where payment_stat = 0 and rs_id = NEW.rs_id order by time_stamp limit 1);
SET NEW.bill_id = IF(NEW.bill_id , NEW.bill_id , pkey);
UPDATE raw_mat_sup rms SET rms.outstanding_bal_payable = rms.outstanding_bal_payable - NEW.amount where rms.rs_id = NEW.rs_id;
END|
and i am getting the following error when i am trying to insert in inflow_pay_done:
/* SQL Error (1048): Column 'bill_id' cannot be null */
you could use a subquery in the BEFORE INSERT trigger for this..
DELIMITER |
DROP TRIGGER `inflow_pay_done_insert`|
CREATE TRIGGER `inflow_pay_done_insert` BEFORE INSERT ON `inflow_pay_done`
FOR EACH ROW
BEGIN
UPDATE raw_mat_sup rms
SET rms.outstanding_bal_payable = rms.outstanding_bal_payable - NEW.amount
WHERE rms.rs_id = NEW.rs_id;
NEW.bill_id = IF(NEW.bill_id,
/* if "bill_id" is provided in INSERT statement, use provided value */
NEW.bill_id,
/* if not, query other table for the correct value */
( /* this subquery is just an example, put your own query here*/
SELECT bill_id FROM inflow_bills
/* find customers newest bill based on newest date and customer id */
WHERE payment_stat = 0 AND rs_id = NEW.rs_id
ORDER BY time_stamp DESC LIMIT 1
)
);
END;
|
delimiter;
UPDATE
Because of a MySQL Bug, this will only work when the column is allowed to be NULL and there is no constraint on the column (-> foreign key). The reason is that MySQL, unlike other DBMS, checks for constraints before a BEFORE INSERT trigger is executed and effectively avoids the execution of the trigger which would correct the data to insert.
The only solution for this, until the behaviour of MySQL changes, is to use a STORED PROCEDURE instead of plain INSERT. The stored procedure is then called with the values that should be inserted. In the procedure, the data correction (like in this case: selecting the right bill_id) is done and then INSERT is executed from within the stored procedure.
UPDATE II
This bug seems to be fixed in 5.7.1. Changelog says:
If a column is declared as NOT NULL, it is not permitted to insert
NULL into the column or update it to NULL. However, this constraint
was enforced even if there was a BEFORE INSERT (or BEFORE UPDATE
trigger) that set the column to a non-NULL value. Now the constraint
is checked at the end of the statement, per the SQL standard.