September 26, 2005 at 6:17 am
Hi,
I have a stored procedure as given below.
CREATE Procedure SpGenerateTableKey_Test
(
@sTable as varchar(100),
@lKeyCount as integer = 1
)
AS
BEGIN
DECLARE @lKey int
SELECT @lKey = 0
BEGIN TRAN
SELECT @lKey = lKey FROM TableKeys (UPDLOCK) WHERE sTable = @Stable
UPDATE TableKeys SET lKey = lKey + @lKeyCount WHERE sTable = @Stable
COMMIT TRAN
RETURN @lKey
END
I got a function to call this procedure as shown below
AlTER FUNCTION fnGenerateTableKey_Test
(
@sTable VARCHAR(100),
@lKeyCount INTEGER
)
RETURNS INT
AS
BEGIN
DECLARE @cmd VARCHAR(250),@lKey INT
SET @cmd = 'osql -E -d ' + db_name() + ' /Q "declare @lkey Int exec spGenerateTableKey_Test ' + @Stable + '" '
EXEC master.dbo.xp_cmdshell @cmd
RETURN @lKey
END
My intension is to return the same value that has been returned by the procedure using a function. But I have no idea how to return the value that the SP return, to the calling function using OSQL. Anybody got any idea?
Why I am so much keen on function is , I can give this function name directly in any insert statement for eg:
insert into entity (lEntityKey) values ( dbo.fnGenerateTableKey_Test ('entity',1)) .
TableKeys table has got two fields , sTable and lKey. sTbale is the name of the tables in the database and lKey is the last ID key of the respective tables.
Regards
Rajesh
September 26, 2005 at 6:59 am
osql can return an integer value by EXIT() fucntion. But you cannot get it in a UDF. You can do it in a SP.
CREATE TABLE #ReturnValue(Rtn nvarchar(255) NULL)
INSERT #ReturnValue EXEC master.dbo.xp_cmdshell @cmd.
SELECT * FROM #ReturnValue
In your @cmd:
DECLARE @RtnVal int
EXEC @RtnVal=YourSP
EXIT(SELECT @RtnVal)
September 26, 2005 at 7:41 am
It seems a very awkward way shelling out to xp_cmdshell and osql only to generate the next number in a sequence.. (although I can appreciate your intentions )
Even if it should have worked to place this inside a function (which it doesn't), have you thought about what would happen when you have to generate FK values in child tables as well? The 'standard' method of handling generation of sequence numbers and inserting those into tables is by stored procedure(s), not by functions.
Also, just wanted to mention that your procedure of generating the new ID's aren't waterproof. As written, it's possible for two concurrent connections to grab the same value as the next ID. (the locking isn't what you expect it to be)
Though, there is a failsafe method of generating countervalues without risking duplicates being produced. The code is below, enjoy.
if object_id('dbo.getNextID') is not null drop proc dbo.getNextID
go
CREATE proc dbo.getNextID @tabname sysname, @nextid int OUTPUT
as
-- file: getNextID.sql
-- why: generic proc that returns the next avilable id-counter for the specified table
-- Usage:
-- declare @varForNewId int
-- exec getNextID 'tableName', @varForNewId OUTPUT
-- @varForNewId now contains the new id....
-- table: uniqueId xxUx - table to keep the counter in - 1 row for each counter and table
--
-- by: Kenneth Wilhelmsson / Cybernetics
-- when: 2001-02-09 - first version
set nocount on
declare @err int,
@rc int
-- check that counter for this table exists
if not exists ( select * from uniqueId where tablename = @tabname )
begin
set @err = 1
goto errhandler
end
-- get the next id
update uniqueId
set @nextid = nextId = nextId + 1
where tablename = @tabname
select @err = @@error, @rc = @@rowcount
if (@err <> 0) goto errhandler
if (@rc <> 1) goto errhandler
return @err
errhandler:
if ( @@trancount > 0 ) ROLLBACK TRANSACTION
declare @errmsg varchar(255)
set @errmsg = 'procedure: ' + object_name(@@procid) + ' *** FATAL ERROR *** '
raiserror(@errmsg, 16, -1) with log
return @err
GO
/Kenneth
September 26, 2005 at 10:13 am
Thank you very much for your valuable reply , both peter and Kenneth.
Kenneth, I can't really agree with what you have said.
it's possible for two concurrent connections to grab the same value as the next ID. (the locking isn't what you expect it to be)
because if you have noticed there is an updLock lock hint on the select statement and update locks are not compatible each other there will not any situation where two concurrent users accessing the same key. More over if not given updLock hint, it would lead to a conversion deadlock situation. Having said that the below shown code would be more ideal to me. I think the logic that has been here is almost the same as of Kenneth.
BEGIN
DECLARE @lKey int
SELECT @lKey = 0
BEGIN TRAN
UPDATE TableKeys SET lKey = lKey + @lKeyCount WHERE sTable = @Stable
SELECT @lKey = lKey - @lKeyCount FROM TableKeys WHERE sTable = @Stable
COMMIT TRAN
RETURN @lKey
END
As you said am I doing some stupid here to generate table keys using UDF as opposed to the traditional way of generating it using SP. may be yes, but sometimes we may have to obey the application development team the business.
I have slightly changed my posted function and Sp in order to achieve the same what I intended to do.
AlTER FUNCTION fnGenerateTableKey_Test
(
@sTable VARCHAR(100),
@lKeyCount INTEGER
)
RETURNS INT
AS
BEGIN
DECLARE @cmd VARCHAR(250),@lKey INT
SET @cmd = 'osql -E -d ' + db_name() + ' /Q "exec spGenerateTableKey_Test ' + @Stable + '" '
EXEC master.dbo.xp_cmdshell @cmd
SELECT @lKey = lKey - @lKeyCount FROM TableKeys WHERE sTable = @Stable
RETURN @lKey
END
CREATE PROCEDURE SpGenerateTableKey_Test
(
@sTable as varchar(100),
@lKeyCount as integer = 1
)
AS
BEGIN
DECLARE @lKey int
SELECT @lKey = 0
BEGIN TRAN
UPDATE TableKeys SET lKey = lKey + @lKeyCount WHERE sTable = @Stable
COMMIT TRAN
RETURN @lKey
END
--below code is given for testing purpose (No blocking or no deadlock identified)
--Connection 1
while 1=1
begin
select dbo.fnGenerateTableKey_Test('entity',1)
end
--Connection 2
while 1=1
begin
select dbo.fnGenerateTableKey_Test('entity',1)
end
I haven't incorporated anything in the SP for the error handling. I have to consider that as well to make it a bullet proof script.
Anyway thank you very much both of you for your invaluable response.
Regards
Rajesh
September 27, 2005 at 8:37 am
Try this:
-- Construct test UId table.
If Object_Id('Test') is not Null Drop Table Test
Select 0[UId] into Test
-- Obtain the next UId.
Declare @i Int
Update Test Set @i=UId,UId=UId+1
-- or --
Update Test Set @i=UId=UId+1
Which "Update" you use depends on whether you want the "Before" or "After" value of the current UId.
October 4, 2005 at 1:40 pm
Rajesh... Your method is a sure recipe for disaster. I would be very interested if you would want to (if possible) share the reasons and motifs that forces you to shell out to osql and pick up the id-value from there. This is a method that has an extremely high chance of generating duplicates!
Yes, you are correct that the updlock hint will block other readers (sry bout that, missed that one ) - BUT... That is all in vain when you're using the convoluted method of shelling out to cmdshell and calling the proc with osql..
I too did use a similar test as your's, and on my laptop managed to generate a staggering 33% of duplicate values! No matter what business says, I cannot see any reason for you to do it this way - it just won't work.
Try this:
create table con1 ( id int not null )
create table con2 ( id int not null )
go
-- in QA window 1
while 1 = 1
begin
insert con1
select dbo.fnGenerateTableKey_Test('customers',1)
end
-- switch to QA window 2 and run:
while 1 = 1
begin
insert con2
select dbo.fnGenerateTableKey_Test('customers',1)
end
When satisfied ( a few seconds ) stop both loops and try:
select *
from con1 join con2
on con1.id = con2.id
When I tried it, after generating 164 id's an amazing 58 of them were duplicates!
/Kenneth
Viewing 6 posts - 1 through 5 (of 5 total)
You must be logged in to reply to this topic. Login to reply