Trigger that creates a row in another table that is referenced by current table - sql

I have 2 tables User and Account. I'd like to have a trigger that creates an account automatically when a user is created. Here is my code:
alter trigger Add_user on [user] for insert as
begin
insert into [account] (name) values ('Main')
declare #newAccountId int, #insertedId int
set #newAccountId = (select scope_identity())
set #insertedId = (select id from inserted)
update [user]
set accountId = #newAccountId
where id = #insertedId
end
I want to have AccountId in the User table be not null however when I try and create a new user it won't let me and I get an error complaining about the not null AccountId column :(

If you make [user].AccountId nullable, it should work.
Also consider following things:
does [account] table contain only column "name"? I.e. is it global
for all users? Then why create new account for each user? If it's
user-specific then add [account].[userId] column.
I would recommend to write stored procedure instead of trigger (first create
account record then user record), it's more explicit and safe. Be
careful with triggers, it's likely to be a surprise for other
developers that inserting user also creates account.

Related

Check if field exists in DB using SQL trigger

I have a database Users that has four fields: Name, Client, ID, Time. Client is an integer (0-99). How to write a trigger that will find latest user from Users (latest according to Time) during Insert and if the Client of this user equals Client of inserted user then I'd like to Rollback
I tried like this:
CREATE TRIGGER DoubledData ON Users
FOR INSERT
AS
DECLARE #client DECIMAL(2)
DECLARE #client_old DECIMAL(2)
DECLARE #name Varchar(50)
SELECT #name = Name from inserted
SELECT #client = Client from inserted
//This doesn't work, "Syntax error near Select":
SELECT #client_old = Select top(1) Client from Users where Name like #name order by Time desc;
IF #client = #client_old
BEGIN
ROLLBACK
END
The problem is that I can assign same values to Client for one user but they can't be one after another (eg for client this order is correct 1-2-3-1-3 -> order is important, but this isn't correct: 1-2-3-3 -> after 2nd occurrence of '3' in a row it needs to be rollbacked)
I'm using MS SQL
[EDIT]
I have found that I can execute it without Select top(1) like:
SELECT #client_old = Client from Users where Name like #name order by Time desc;
But the trigger doesn't execute afer insert
First, you clearly don't understand triggers in SQL Server and the inserted pseudo-tables. These can have more than one row, so your code will fail when multiple rows are inserted. Sadly, there is no compile check for this situation. And code can unexpectedly fail (even in production, alas).
Second, the right way to do this is probably with a unique constraint. That would be:
alter table users
add constraint unq_users_name_client unique (name, client);
This would ensure no duplication, so it is a stronger condition than your trigger.

Stored Procedure/SQL script that allows me to add a record to a table

I have 2 tables, one with all the email data, and another with all the specific member email data that one creates a row if an email has been read.
Once an email has been read by a member its added to the Member_email_read table (which is created and populated based on all read emails).
I am trying to set (on mass) all of the messages to read (this would populate the Member_email_read table) but whilst I can add them one at a time
(see the stored procedue below), I am unable to add them on mass.
The two tables are Email, which holds a record for every email into the system. The other table is a table of all email that the member has read. Each time an email
is read a record is added to the Member_email_read table. They are assiocited on the message_id (and both should use the same user_id). The two tables are as follows -
SELECT [member_email_id]
,[member_email_FK_message_id]
,[member_email_FK_user_id]
,[member_email_status]
,[member_email_read_datetime]
,[member_email_delete_datetime]
FROM [MemberData].[dbo].[Member_email_read]
SELECT[message_id]
,[email_catlogue_num]
,[email_FK_user_id]
,[Email_time]
,[email_content]
,[Email_created_date]
FROM [MemberData].[dbo].[Email]
To set all the messages (for a certain user) to unread all I would have to do is delete every record from that table for that user, which can be done with the following:
DELETE FROM [MemberData].[dbo].[Member_email_read]
WHERE [member_email_FK_message_id_FK_user_id] ='2';
I am basically looking for the reverse of this delete.
I have created a Stored procedure that allows for the setting of ONE specific email to be set to read, however this stored procedure (when executed) requires the member to enter a
email_id, message_id, user_id, status, read_datetime & delete_datetime.
CREATE PROCEDURE [dbo].[set_member_email_to_read]
#member_email_id int,
#member_email_FK_message_id int,
#member_email_FK_user_id int
#member_email_status varchar(1),
#member_email_read_datetime dateTime,
#member_email_delete_datetime dateTime
as
if not exists (Select * from [dbo].[Member_email_read] where [member_email_FK_message_id] = #member_email_FK_message_id) begin
insert into [dbo].[Member_email_read]
(
[member_email_FK_message_id]
,[member_email_FK_user_id]
,[member_email_status]
,[member_email_read_datetime]
,[member_email_delete_datetime]
)
values
(
#member_email_FK_message_id,
#member_email_FK_user_id
#member_email_status,
#member_email_read_datetime,
#member_email_delete_datetime
)
SELECT Convert(int,SCOPE_IDENTITY()) As InsertedID
end else begin
update [dbo].[Member_email_read] set
[member_email_FK_message_id] = #member_email_FK_message_id
,[member_email_FK_user_id] = #member_email_FK_user_id
,[member_email_status] = #member_email_status
,[member_email_read_datetime] = #member_email_read_datetime
,[member_email_delete_datetime] = #member_email_delete_datetime
where [member_email_FK_user_id] = #member_email_FK_user_id
if (##ERROR = 0) begin
SELECT Convert(int,#member_email_FK_user_id) As InsertedID
end
end
GO
I was hoping to create a stored procedure (or general SQL script) that would allow me to enter in
a user_id and then allow for all emails for that user to change from unread to read (populate the Member_email_read table).
You can try using MERGE to perform a bulk insert/update from your Email table into your Member_email_read table:
MERGE [MemberData].[dbo].[Member_email_read] AS tgt
USING (
SELECT message_id, user_id, 'R', null, CURRENT_TIMESTAMP
FROM [MemberData].[dbo].[Email]
WHERE user_id = #UserId
) AS src (MessageId, UserId, Status, ReadDate, DeleteDate)
ON (
tgt.member_email_FK_message_id = src.message_id
AND tgt.member_email_FK_user_id = src.user_id
)
WHEN MATCHED THEN
UPDATE SET tgt.member_email_status = src.Status,
tgt.member_email_read_datetime = src.ReadDate,
tgt.member_email_delete_datetime = src.DeleteDate
WHEN NOT MATCHED THEN
INSERT (member_email_FK_message_id, member_email_FK_user_id,
member_email_status, member_email_read_datetime, member_email_delete_datetime)
VALUES (src.MessageId, src.UserId, src.Status, src.ReadDate, src.DeleteDate)
;
This should work, though as I mentioned in my comment above, you should rethink your table design and simply add a read_datetime and delete_datetime (possibly a status column if you really need that as well) to your Email table, rather than having a whole separate table simply to hold records identifying a delete status.

SQL trigger - Always update or conditionally

In my scenario a single password in table B needs to be updated when this password changes in table A. I've been given a trigger which does this, but the trigger always updates the value, even when the password in table A isn't modified, but one of the other fields is modified.
This seems like overkill to me, because the trigger can be modified to update only when specifically the password field is modified.
Is the provided solution decent, or would it be better (performance wise mainly) to change the trigger and add a condition on which to actually modify the row. I can imagine the cost of conditionally updating being greater than blindly changing the value every time.
Current code:
CREATE TRIGGER [UserSync]
ON [dbo].[Import]
FOR UPDATE
AS
BEGIN
DECLARE #UserName NVARCHAR(128)
DECLARE #Password NVARCHAR(128)
SELECT #UserName=Username
, #Password=Password
FROM INSERTED
UPDATE UserLogin
SET Password = #Password
WHERE Name = #UserName
END
A better was to write this, to a) allow for updates of multiple rows as per #marc_s and b) only update where it has changed, is:
CREATE TRIGGER [UserSync]
ON [dbo].[Import]
FOR UPDATE
AS
BEGIN
UPDATE UserLogin
SET Password = i.password
from UserLogon u
inner join inserted i on i.Name = u.Name
inner join deleted d on d.Name = i.Name
WHERE i.password <> u.password
END
So, for each user in the transaction the old (deleted) and the new (inserted) are matched against the underlying table (UserLogon). Where the new password is different to the old, the underlying table is updated.

SQL IF/Case in stored procedure

I am trying to create procedure which will insert two values in my Pickup table.
create procedure sp_InsertPickup
#ClientID int,
#PickupDate date
as
insert into Pickup (ClientID ,PickupDate )values (#ClientID,#PickupDate)
However I need check if this client already did pickup in this month (record in table)it should not insert any new records it table.
example if this data in table
ClientID PickupDate
11 03-01-2013
And I want insert ClientId 11 and new PickupDate 03-24-2013 it should just not insert because this person already did pickup this month.
Any Ideas how to implement it ?
So in that case, use a IF NOT EXISTS:
CREATE PROCEDURE dbo.InsertPickup
#ClientID int,
#PickupDate date
AS
IF NOT EXISTS (SELECT * FROM Pickup
WHERE ClientID = #ClientID
AND MONTH(PickupDate) = MONTH(#PickupDate)
AND YEAR(PickupDate) = YEAR(#PickupDate) )
INSERT INTO Pickup (ClientID, PickupDate)
VALUES (#ClientID, #PickupDate)
You might want to somehow indicate to the caller that there was no data inserted, due to the fact it already exists....
As a side note: you should not use the sp_ prefix for your stored procedures. Microsoft has reserved that prefix for its own use (see Naming Stored Procedures), and you do run the risk of a name clash sometime in the future. It's also bad for your stored procedure performance. It's best to just simply avoid sp_ and use something else as a prefix - or no prefix at all!
The safest way to do this is to either use merge or to put a constraint on the table and trap for the error.
The reason merge is safer is because it is an atomic transaction. Checking for existence and then doing the insert is dangerous, because someone else might have already inserted (or deleted) the row. You can start playing with transaction semantics in the stored procedure, but why bother when SQL Server provides merge:
merge Pickup as target
using (select #PickupDate, #ClientId) as source(PickupDate, ClientId)
on target.clientId = source.ClientId and year(source.PickupDate) = year(target.PickupDate) and month(source.PickupDate) = month(target.PickupDate)
when NOT MATCHED then
insert(PickupDate, ClientId) values(source.PickupDate, source,ClientId);
You can read more about merge an.
This is how you can implement it using IF NOT EXISTS
create procedure sp_InsertPickup
#ClientID int,
#PickupDate date
as
IF NOT EXISTS (SELECT * FROM Pickup
WHERE ClientID = #ClientID
AND DATEPART(mm,PickupDate) = DATEPART(mm,#PickupDate)
AND DATEPART(yy,PickupDate) = DATEPART(yy,#PickupDate))
BEGIN
insert into Pickup (ClientID ,PickupDate )values (#ClientID,#PickupDate)
END
begin
end

SQL Table Locking

I have an SQL Server locking question regarding an application we have in house. The application takes submissions of data and persists them into an SQL Server table. Each submission is also assigned a special catalog number (unrelated to the identity field in the table) which is a sequential alpha numeric number. These numbers are pulled from another table and are not generated at run time. So the steps are
Insert Data into Submission Table
Grab next Unassigned Catalog
Number from Catalog Table
Assign the Catalog Number to the
Submission in the Submission table
All these steps happen sequentially in the same stored procedure.
Its, rate but sometimes we manage to get two submission at the same second and they both get assigned the same Catalog Number which causes a localized version of the Apocalypse in our company for a small while.
What can we do to limit the over assignment of the catalog numbers?
When getting your next catalog number, use row locking to protect the time between you finding it and marking it as in use, e.g.:
set transaction isolation level REPEATABLE READ
begin transaction
select top 1 #catalog_number = catalog_number
from catalog_numbers with (updlock,rowlock)
where assigned = 0
update catalog_numbers set assigned = 1 where catalog_number = :catalog_number
commit transaction
You could use an identity field to produce the catalog numbers, that way you can safely create and get the number:
insert into Catalog () values ()
set #CatalogNumber = scope_identity()
The scope_identity function will return the id of the last record created in the same session, so separate sessions can create records at the same time and still end up with the correct id.
If you can't use an identity field to create the catalog numbers, you have to use a transaction to make sure that you can determine the next number and create it without another session accessing the table.
I like araqnid's response. You could also use an insert trigger on the submission table to accomplish this. The trigger would be in the scope of the insert, and you would effectively embed the logic to assign the catalog_number in the trigger. Just wanted to put your options up here.
Here's the easy solution. No race condition. No blocking from a restrictive transaction isolation level. Probably won't work in SQL dialects other than T-SQL, though.
I assume their is some outside force at work to keep your catalog number table populated with unassigned catalog numbers.
This technique should work for you: just do the same sort of "interlocked update" that retrieves a value, something like:
update top 1 CatalogNumber
set in_use = 1 ,
#newCatalogNumber = catalog_number
from CatalogNumber
where in_use = 0
Anyway, the following stored procedure just just ticks up a number on each execution and hands back the previous one. If you want fancier value, add a computed column that applies the transform of choice to the incrementing value to get the desired value.
drop table dbo.PrimaryKeyGenerator
go
create table dbo.PrimaryKeyGenerator
(
id varchar(100) not null ,
current_value int not null default(1) ,
constraint PrimaryKeyGenerator_PK primary key clustered ( id ) ,
)
go
drop procedure dbo.GetNewPrimaryKey
go
create procedure dbo.GetNewPrimaryKey
#name varchar(100)
as
set nocount on
set ansi_nulls on
set concat_null_yields_null on
set xact_abort on
declare
#uniqueValue int
--
-- put the supplied key in canonical form
--
set #name = ltrim(rtrim(lower(#name)))
--
-- if the name isn't already defined in the table, define it.
--
insert dbo.PrimaryKeyGenerator ( id )
select id = #name
where not exists ( select *
from dbo.PrimaryKeyGenerator pkg
where pkg.id = #name
)
--
-- now, an interlocked update to get the current value and increment the table
--
update PrimaryKeyGenerator
set #uniqueValue = current_value ,
current_value = current_value + 1
where id = #name
--
-- return the new unique value to the caller
--
return #uniqueValue
go
To use it:
declare #pk int
exec #pk = dbo.GetNewPrimaryKey 'foobar'
select #pk
Trivial to mod it to return a result set or return the value via an OUTPUT parameter.