Home > database >  Stored procedure only runs once
Stored procedure only runs once

Time:02-11

I have a SQL Server stored procedure:

CREATE PROCEDURE dbo.GetNextNumber
    @NextNumber int OUT
AS
    UPDATE SystemSettings 
    SET REGISTRY_NUMBER = REGISTRY_NUMBER   1  
    WHERE ID = 1;

    SELECT @NextRegistryNumber = REGISTRY_NUMBER
    FROM SystemSettings
    WHERE ID = 1;
  
    SELECT 'NextNumber' = @NextRegistryNumber;

I call this from a C# API method:

public int GetNextRegistryNumberFC()
{
    string procedure = "GetNextNumber";
        
    // Create ADO.NET objects.
    // SqlConnection con = new SqlConnection(connectionString);
    // SqlCommand cmd = new SqlCommand(procedure, con);

    using (var con = new SqlConnection(ConfigurationManager.ConnectionStrings[<ConnectionString>].ConnectionString))
    using (var cmd = new SqlCommand(procedure, con))

    try
    {
        // Configure command and add input parameters.
        cmd.CommandType = CommandType.StoredProcedure;

        // Add the output parameter.               
        cmd.Parameters.Add("@NextNumber", SqlDbType.Int).Direction = ParameterDirection.Output;                

        // Execute the command.
        con.Open();
        cmd.ExecuteNonQuery();

        NextRegistryNumber = Convert.ToInt32(cmd.Parameters["@NextNumber"].Value);
    }
    catch (Exception ex)
    {
        throw;
    }
    finally
    {
        cmd.Dispose();
        con.Close();
        con.Dispose();
    }

    return NextRegistryNumber;
}

This works the first time I run it. After that it just return the same number each time instead of incrementing it. I am not sure what I happening. If I run the stored procedure by it self it works each time. Does anyone know what is going wrong here?

CodePudding user response:

This stored procedure will produce unexpected results if it's called by two or more connections at the same time. Both executions will increment the field then both return the same value. Even if this was fixed, the update query creates a hotspot in a single row, with concurrent requests blocking while they wait for each other to update that row.

To avoid this, SQL Server (and other databases) have SEQUENCE objects that can be incremented atomically with minimal locking :

CREATE SEQUENCE Registration_Numbers START WITH 1;

The NEXT VALUE FOR function will increment a sequence and return the new value :

SELECT NEXT VALUE FOR Registration_Numbers;

This query is so short it doesn't need a stored procedure :

var sql="SELECT NEXT VALUE FOR Registration_Numbers;";
using(var con=new SqlConnection(connectionString))
using(var cmd=new SqlCommand(sql,con))
{
    var new_id=(long)cmd.ExecuteScalar();
    return new_id;
}

There's no need for try/catch here. A using block ensures the connection and command objects will be disposed even if an exception occurs. using works even in cases where finally wouldn't be called.

Rethrowing isn't needed either. The following catch block is no different than no catch at all.

catch (Exception ex)
{
    throw;
}

Such a block would make sense if the exception was processed, eg logged.

Atomic update and read

If using a table is absolutely necessary (why?), the OUTPUT clause can be used to return the new value right from the UPDATE command:

CREATE PROCEDURE dbo.GetNextNumber
AS
    UPDATE SystemSettings 
    SET REGISTRY_NUMBER = REGISTRY_NUMBER   1  
    OUTPUT inserted.REGISTRY_NUMBER
    WHERE ID = 1;

The same code can be used to call the stored procedure. Only the the CommandType needs to change

var proc_name=" dbo.GetNextNumber";
using(var con=new SqlConnection(connectionString))
using(var cmd=new SqlCommand(proc_name,con))
{
    cmd.CommandType = CommandType.StoredProcedure;

    var new_id=(long)cmd.ExecuteScalar();
    return new_id;
}

CodePudding user response:

Is this a typo?

SELECT 'NextNumber' = @NextRegistryNumber;

This would output a table with a column called 'NextNumber' with the value in @NextRegistryNumber ie: you've done a column alias.

It should be:

SELECT @NextNumber = @NextRegistryNumber;

OR

SET @NextNumber = @NextRegistryNumber;
  • Related