Sql Server Database stored procedure triggered when a value becomes 0 - sql-server

I have a column in one of my tables that acts as a countdown. So as the current datetime approaches the endtime, it works its way down to 0. The countdown's value becomes 0 once the endtime is reached. When this happens, I would like it to trigger a stored procedure. Does anyone know how I could create a trigger such as this?
Table:
CREATE TABLE t1 (
id int IDENTITY,
enddate datetime NOT NULL,
daysleft AS (DATEDIFF(dd, GETDATE(), enddate))
);
INSERT INTO t1 (enddate)
VALUES(DATEADD(dd, 1, GETDATE())),
(DATEADD(dd, 2, GETDATE())),
(DATEADD(dd, 15, GETDATE()))
I haven't created the procedure yet, only because it would be useless if I can't create my required trigger.

I don't think there is a construct in SQL Server that allows you to run a stored procedure when the value of data equals user defined value. The reason behind this is if something as such were to be made, the performance of SQL Server would be tremendously impacted.
I would instead recommend you to consider writing a Windows Service that periodically wakes up and checks whether there are any rows in your table that have expired and then invoke your intended stored procedure on them. I can give you a sample of how to achieve this if you would like.
EDIT: A sample implementation of Windows Service
Okay so as I said above, there is no way of being notified when a particular data has reached its expiry time. So instead of being in the push notification model, we are working in a pull notification model where we periodically query the database for expired rows (e.g. something like "SELECT id FROM t1 WHERE enddate = #todayDate")
Now the key part of this solution is we need a periodic service. This has been demonstrated in the another user's answer here. The method where we will do our SQL operations is private void timer_Elapsed(...).
private void timer_Elapsed(object sender, System.Timer.ElapsedEventArgs e)
{
// since I am not sure whether you are accessing your database using ADO.NET
// (i.e. SqlConnection, etc.) or Entity Framework (i.e. DbContext),
// I will assume ADO.NET from here on
// retrieve all rows that have expired
using (var connection = new SqlConnection(connectionString))
{
connection.Open();
var command = new SqlCommand("SELECT id FROM t1 WHERE enddate = #todayDate", connection);
var paramDate = new SqlParameter("#todayDate", DateTime.Now.Date);
command.Parameters.Add(paramDate);
var reader = command.ExecuteReader();
while (reader.Read())
{
var storedProcCommand = new SqlCommand("EXEC CleanUpExpiredRow #id", connection);
var paramId = new SqlParameter("#id", reader.GetInt32(0));
storedProcCommand.Parameters.Add(paramId);
command.ExecuteNonQuery();
}
}
}
NOTE None of this code is tested.

Related

Replacing SQL Server IDENTITY Due to Autogenerated Issue

I have an Invoice Database that contains an ID IDENTITY that SQL Server is autogenerating by an increment of one (+1) each time a new record is created by a LINQ Insert.
The code that I am currently using to create a new record is posted below and the Incremental ID is autogenerated by SQL Server.
public async Task<IActionResult> Create([Bind(
"PurchaseOrder,InvDate,DelDate,PaidDate,AgentName,FullName,FirstName, LastName,CustId,CompanyName,ClientRole,Email,Phone,Address," +
"City,State,Zip,Country,ProdCode,Description,Quantity,UnitPrice,LineTotal,OrderTotal,Tax,Discount,Credit," +
"Shipping,GrandTotal,Deposit,AmtDue,DiscAmt,Notes,Published,Year,Expenses,ProjectDescription,ClientProvision")]
CreateNewOrderViewModel cnq)
{
int invId;
try
{
await _context.AddAsync(cnq);
await _context.SaveChangesAsync();
}
catch (InvalidCastException e)
{
ViewBag.Result = $"Database insert failed with: {e}";
return View();
}
}
My issue is with the SQL Server ID IDENTITY. Every time the server is rebooted, my ID IDENTITY value increases by a factor of 1000 instead of the default value of 1, which for example, changes/increases the next record that I created by a factor of 1000. Hence, if my last record was 1001, the next record that is created will be 2001, instead of 1002. This behavior continues every time the server is updated and needs to be rebooted. I searched for an answer and discovered that the issue is a SQL Server bug that is based on the Cached protocol that remembers the latest ID values.
Since I am on a Shared Hosting Server and do not have full control of the Database, I only have DBO to my own database. I was wondering if there was a way for me to use LINQ to generate the incremental value for a new InvID column that I can then use as the record ID, instead of the SQL Server generated value.
If you don't have control over database to handle sql server identity gap issue, you need to manually read the largest Id from the table and increment it by 1 for the new record.
Sample code to retrieve the last Id:
int lastId = _context.TableA.OrderByDescending(t => t.Id).FirstOrDefault().Id;
int newId = lastId + 1;
// Add the new record with newId
Reading several recommendations, I decided to check and reseed the ID before running the LINQ query.
using (var connection = new SqlConnection(_data.DATA))
{
connection.Open();
try
{
var seed = new SqlCommand("CreateIDSeed", connection)
{
CommandType = CommandType.StoredProcedure
};
seed.ExecuteNonQuery();
await _context.AddAsync(cnq);
await _context.SaveChangesAsync();
}
catch (InvalidCastException e)
{
ViewBag.Result = $"Database insert failed with: {e}";
return View();
}
}
Where the CreateIDSeed looks like this:
CREATE PROCEDURE [dbo].[CreateIDSeed]
AS
BEGIN
SET NOCOUNT ON;
declare #newId int
select #newId = max(ID) from dbo.CreateNewOrderViewModel
DBCC CheckIdent('dbo.CreateNewOrderViewModel', RESEED, #newId)
END
GO
I tried inserting a few test records and it seems to be working, but I will know more when the Server is rebooted.

F# FSharp.Data.SqlClient not recognizing multiple return tables from Stored Procedure

I am not sure if this is possible but I have not been able to come across clear documentation for this use case. I am using F# 4 and the FSharp.Data.SqlClient library to connect to SQL Server 2016. I am wanting to call a stored procedure that returns multiple tables and turn those tables into the corresponding records. In this case the first table is made up of items and the second table is made up of customers.
My instinct is that it should look something like this:
let items, customers = cmd.Execute()
My gut is that items would be an IEnumerable<item> and customers would be an IEnumerable<customer> where item and customer are both Record types. What it appears is happening though is that FSharp.Data.SqlClient is only seeing the first returned table from the stored procedure. I am working on a SQL Server 2016 Developer instance. Here is the T-SQL to setup the example:
create table Item (
ItemID int identity(1, 1) primary key,
ItemName nvarchar(50)
)
go
create table Customer (
CustomerID int identity(1, 1) primary key,
CustomerName nvarchar(50)
)
go
insert into Item (ItemName) values ('A');
insert into Item (ItemName) values ('B');
insert into Item (ItemName) values ('C');
insert into Customer (CustomerName) values ('Gary');
insert into Customer (CustomerName) values ('Sergei');
insert into Customer (CustomerName) values ('Elise');
go
create procedure dbo.ExampleProcedure
as
begin
set nocount on;
select
ItemID,
ItemName
from Item
select
CustomerID,
CustomerName
from Customer
end;
And here is the F# script that I am testing with. It shows what I would like to be able to do but I get a compile error on the last line:
#r "../packages/FSharp.Data.SqlClient.1.8.2/lib/net40/FSharp.Data.SqlClient.dll"
#r "../packages/FSharp.Data.2.3.2/lib/net40/FSharp.Data.dll"
#r "System.Xml.Linq.dll"
open FSharp.Data
[<Literal>]
let connStr =
"Data Source=**connection string**;"
type queryExample = SqlProgrammabilityProvider<connStr>
do
use cmd = new queryExample.dbo.ExampleProcedure(connStr)
let items, customers = cmd.Execute()
I am wanting items to correspond to the first returned table and customers to correspond to the second returned table. The intellisense suggests that FSharp.Data.SqlClient is only seeing the first table. When I hover over cmd.Execute() the popup says "This expression was expected to have type 'a*'b but here has type System.Collections.Generic.IEnumerable<SqlProgrammabilityProvider<...>.dbo.ExampleProcedure.Record>". If I do the following I get access to the Items query in the stored procedure:
// Learn more about F# at http://fsharp.org. See the 'F# Tutorial' project
// for more guidance on F# programming.
#r "../packages/FSharp.Data.SqlClient.1.8.2/lib/net40/FSharp.Data.SqlClient.dll"
#r "../packages/FSharp.Data.2.3.2/lib/net40/FSharp.Data.dll"
#r "System.Xml.Linq.dll"
open FSharp.Data
[<Literal>]
let connStr =
"Data Source=**connection string**;"
type queryExample = SqlProgrammabilityProvider<connStr>
do
use cmd = new queryExample.dbo.ExampleProcedure(connStr)
for item in cmd.Execute() do
printfn "%A" item.ItemID
Is this even possible? Is my approach wrong? I could not find clear documentation on this use case but I thought it would be common enough it would be covered.
Update
Just to clarify what I am trying to achieve I am showing how I solve this in C#. In C# I create a DataSet object and populate it with the results of the Stored Procedure. From there I pick out the individual tables to work with. After extracting the tables I then use LINQ to transform the rows into the corresponding objects. It often looks something like the following:
using System.Data;
using System.Data.SqlClient;
var connStr = "**connection string**"
var sqlConnection = new SqlConnection(connStr );
var sqlCommand = new SqlCommand("ExampleProcedure", sqlConnection);
sqlCommand.CommandType = CommandType.StoredProcedure;
var dataSet = new DataSet();
var adapter = new SqlDataAdapter(sqlCommand);
adapter.Fill(dataSet);
var itemsTable = dataSet.Tables[0];
// Turn the itemsTable into a List<Item> using LINQ here
var customersTable = dataSet.Tables[1];
// Turn the customersTable into List<Customer> using LINQ here
I find this to be overly verbose for such a simple thing as extracting the individual tables but perhaps I am too sensitive to code clutter. I know that F# must have a more elegant and terse way to express this.
I don't know F#, however this is a data access problem.
When a stored procedure returns multiple resultsets, you need to access they in sequence, one by one.
cmd.ExecuteReader() returns an instance of a datareader pointing to the first resultset. You need to process this resultset, may be filling a list with instances of a custom class, than you call the method "NextResult" and you will have access to the next resultset and so on.
A reference for the method "NextResult": https://msdn.microsoft.com/pt-br/library/system.data.sqlclient.sqldatareader.nextresult(v=vs.110).aspx

Same data is inserted during insert

I have couple insert queries which are merged in transaction. First of that insert is to create new product articel number incrementing the most higher in table by one. Unfortunetly i just noticed that mostly during tests if for instance two users from two diffrent applications click button which trigger my transaction's method they could get same new product number. How can avoid that situation? Is there something like lock on first insertion so that if first user accessing table to insert restrict other's user/s about their insertion so they have to wait in queue after first user insert is finished? Is there something like that? Besides i thought if someone inserts other users are not able to insert. I made comments in code you to understand.
Part of my transaction query below:
Public Sub ProcessArticle(ByRef artikel As ArticlesVariations)
Dim strcon = New AppSettingsReader().GetValue("ConnectionString", GetType(System.String)).ToString()
Using connection As New SqlConnection(strcon)
connection.Open()
Using transaction = connection.BeginTransaction()
Try
For Each kvp As KeyValuePair(Of Integer, Artikel) In artikel.collection
articleIndex = kvp.Key
Dim art As Artikel = kvp.Value
Using cmd As New SqlCommand("INSERT INTO tbArtikel (Nummer) VALUES (#Nummer);Select Scope_Identity()", transaction.Connection)
cmd.CommandType = CommandType.Text
cmd.Connection = connection
cmd.Transaction = transaction
'Get next product number from table tbArtikel (this will be new product number)'
Dim NewArtNummer as String = New DALArtikel().GetNewArtikelNumber(transaction)
art.Nummer = NewArtNummer
cmd.Parameters.AddWithValue("#Nummer", art.Nummer)
'Get inserted product id for other diffrent inserts below'
newArticleRowId = CInt(cmd.ExecuteScalar())
'....
other INSERTs queries to other tables ...
...'
transaction.Commit()
Catch ex As Exception
transaction.Rollback()
Throw 'Rethrow exception.'
End Try
End Using
End Using
End Sub
Just about the only way to assure that users are not assigned the same values is to issue them from the server when the row is inserted. It is the entire premise behind the server issuing AI values for PKs.
BUT since your thing is a multi-segment, "numeric string" that presents a problem. Rather than tearing the string apart to find the Max()+1 for one segment with a WHERE clause on parts of the string. Consider something like this:
Start with a table used to increment and issue the values:
{DocId Int, SegmentB int, SegmentC Int}
This will simply track the values to use in the other table. Then a stored procedure to create/increment a new code (MySQL - this is a conceptual answer):
CREATE DEFINER=`root`#`localhost` PROCEDURE `GetNextProductCode`(in docId int,
in Minr int,
in Rev int
)
BEGIN
SET #maxR = 0;
SET #retCode ='';
if Minr =-1 then
Start transaction;
SET #maxR = (SELECT Max(SegmentB) FROM articlecode WHERE MainId = docId) + 1;
UPDATE articlecode SET SegmentB = #maxR WHERE MainId = docId;
Commit;
Select concat(Cast(docId As char) , '.',
Cast(#maxR AS char) , '.',
Cast(Rev As char)
);
end if;
END
This is a rough idea of the process. As such, it only works on the second segment (I dunno what happens when you create a NEW SegmentB - does SegmentC reset to 1???). The idea is:
pass numbers so there is no need to tear up a string
pass -1 for the segment you need the next value for
the sp gets the Max()+1 and updates the counter table so the next user will get a new value
If for some reason you end up not saving the row, there will be gaps
the sp uses a transaction (probably only needs to protect the update) so that only 1 update can happen at a time
returns the new code. it could just return 2 values, but your going to glue them together anyway
There is much To Do:
It only does SegmentB
For a NEW DocId (-1), insert a new row with 1000 and 1(?) defaults
Same for a NEW segmentB (whatever it is): insert a new row for that DocId with default values
To get a new code before you insert a row:
cmd.CommandType = CommandType.StoredProcedure
cmd.Parameters.Add("docId", MySqlDbType.Int32).Value = 3
cmd.Parameters.Add("Minr", MySqlDbType.Int32).Value = -1
cmd.Parameters.Add("Rev", MySqlDbType.Int32).Value = 1
dbcon.Open()
Using rdr = cmd.ExecuteReader()
rdr.Read()
Console.WriteLine(rdr(0))
End Using
The obvious downside is that each insert requires you to hit the DB in order to...well save to the DB. If they were int values it could be a Trigger.
I'm a SQL developer and my VB skills are about fifteen years out of date, but instead of creating the incremented number yourself in VB just let SQL generate them with an IDENTITY field. SQL will never allow duplicates and then you just need to return the SCOPE_IDENTITY():
ALTER TABLE dbo.tbArtikel
ADD [ArtikelID] INT IDENTITY(1,1) PRIMARY KEY;
I have two suggestions:
First suggestion: move your code to a stored procedure this way all your users will execute the same transaction where you can set your isolation level the way you want. Read This.
Second suggestion: I would create a unique index on your field Nummer. This way when I try to insert a duplicate value it will raise an error that I can deal with it by telling the user that he need to retry the same operation or retry it automatically.
Trying to lock the record or the table for your operation is not advisable, however you can check this article on code project you might find what you are looking for. Make sure that you provide a mechanism of releasing all locks if your program stops at the middle of the transaction.

Correct method of deleting over 2100 rows (by ID) with Dapper

I am trying to use Dapper support my data access for my server app.
My server app has another application that drops records into my database at a rate of 400 per minute.
My app pulls them out in batches, processes them, and then deletes them from the database.
Since data continues to flow into the database while I am processing, I don't have a good way to say delete from myTable where allProcessed = true.
However, I do know the PK value of the rows to delete. So I want to do a delete from myTable where Id in #listToDelete
Problem is that if my server goes down for even 6 mintues, then I have over 2100 rows to delete.
Since Dapper takes my #listToDelete and turns each one into a parameter, my call to delete fails. (Causing my data purging to get even further behind.)
What is the best way to deal with this in Dapper?
NOTES:
I have looked at Tabled Valued Parameters but from what I can see, they are not very performant. This piece of my architecture is the bottle neck of my system and I need to be very very fast.
One option is to create a temp table on the server and then use the bulk load facility to upload all the IDs into that table at once. Then use a join, EXISTS or IN clause to delete only the records that you uploaded into your temp table.
Bulk loads are a well-optimized path in SQL Server and it should be very fast.
For example:
Execute the statement CREATE TABLE #RowsToDelete(ID INT PRIMARY KEY)
Use a bulk load to insert keys into #RowsToDelete
Execute DELETE FROM myTable where Id IN (SELECT ID FROM #RowsToDelete)
Execute DROP TABLE #RowsToDelte (the table will also be automatically dropped if you close the session)
(Assuming Dapper) code example:
conn.Open();
var columnName = "ID";
conn.Execute(string.Format("CREATE TABLE #{0}s({0} INT PRIMARY KEY)", columnName));
using (var bulkCopy = new SqlBulkCopy(conn))
{
bulkCopy.BatchSize = ids.Count;
bulkCopy.DestinationTableName = string.Format("#{0}s", columnName);
var table = new DataTable();
table.Columns.Add(columnName, typeof (int));
bulkCopy.ColumnMappings.Add(columnName, columnName);
foreach (var id in ids)
{
table.Rows.Add(id);
}
bulkCopy.WriteToServer(table);
}
//or do other things with your table instead of deleting here
conn.Execute(string.Format(#"DELETE FROM myTable where Id IN
(SELECT {0} FROM #{0}s", columnName));
conn.Execute(string.Format("DROP TABLE #{0}s", columnName));
To get this code working, I went dark side.
Since Dapper makes my list into parameters. And SQL Server can't handle a lot of parameters. (I have never needed even double digit parameters before). I had to go with Dynamic SQL.
So here was my solution:
string listOfIdsJoined = "("+String.Join(",", listOfIds.ToArray())+")";
connection.Execute("delete from myTable where Id in " + listOfIdsJoined);
Before everyone grabs the their torches and pitchforks, let me explain.
This code runs on a server whose only input is a data feed from a Mainframe system.
The list I am dynamically creating is a list of longs/bigints.
The longs/bigints are from an Identity column.
I know constructing dynamic SQL is bad juju, but in this case, I just can't see how it leads to a security risk.
Dapper request the List of object having parameter as a property so in above case a list of object having Id as property will work.
connection.Execute("delete from myTable where Id in (#Id)", listOfIds.AsEnumerable().Select(i=> new { Id = i }).ToList());
This will work.

How to add #temp tables

I have formed few temp tables in my query (TSQL) like - #temphold1, #temphold2, #temphold3..... #temphold10. Each temp table has different schema (different columns) each derived by grouping data from different tables with specific conditions. I need to determine a way to carry all these temp tables to the User Interface and display each table seperately. Is there a way I can add all temp tables with specific indexer that i can retrieve at the User Interface.
thanks for any reply.
No, there is no such indexer.
However, SQL Server and ADO.NET support returning multiple result sets by selecting each table in turn.
See this howto on MSDN (How To Handle Multiple Results by Using the DataReader in Visual C# .NET).
So, in your stored procedure:
-- after populating your temp tables:
SELECT * FROM #table1
SELECT * FROM #table2
SELECT * FROM #table3
In essence, after reading the first recordset, you call NextResult() on the DataReader in order to get the results of the next select:
while(dr.Read())
{
// process data from #table1
}
dr.NextResult();
while(dr.Read())
{
// process data from #table2
}
dr.NextResult();
while(dr.Read())
{
// process data from #table3
}
If you're returning results to C#, you can do it with a DataAdapter like this:
using (SqlConnection conn = new SqlConnection("your connection string")) {
SqlParameter[] sqlParams = new SqlParameter[] {
new SqlParameter("#param1",10),
new SqlParameter("#param2","test")
};
conn.Open();
SqlDataAdapter sa = new SqlDataAdapter("spStoredProcName", conn);
sa.SelectCommand.CommandType = CommandType.StoredProcedure;
sa.SelectCommand.Parameters.AddRange(sqlParams);
DataSet ds = new DataSet();
sa.Fill(ds);
//ds.Tables[0] == first table
//ds.Tables[1] == second table
//... etc.
//Do whatever you need to here
}

Resources