Stored Procedure'ler: Parametreler, Hata Yönetimi, Yetki Zinciri ve Güvenli Dynamic SQL
Doğru stored procedure iskeleti SET NOCOUNT ON, SET XACT_ABORT ON, TRY/CATCH içinde ROLLBACK ve parametresiz THROW'dur; aksi hâlde bir CHECK hatası yarım siparişi commit edebilir. Output parameter ve return code'un Dapper'dan okunması, uygulama kullanıcısına yalnız EXECUTE veren ownership chaining, allowlist + QUOTENAME + sp_executesql ile güvenli dynamic SQL ve dynamic SQL'in kırdığı zinciri module signing ile onarmak gerçek çıktılarla.
Sürüm notu: Bu yazıdaki SQL (Structured Query Language) örnekleri SQL Server 2025 (17.0.5035.5, Cumulative Update 10) üzerinde, compatibility level 170 ile çalıştırılmıştır; .NET örneği .NET 10, Microsoft.Data.SqlClient 7.1.1 ve Dapper 2.1.89 ile derlenip denenmiştir.
Önce Tahmin Et: Stok Yetmezse Sipariş Oluşur mu?
Bookstore'da sipariş oluşturan ilk stored procedure'ü herkes aşağı yukarı böyle yazar: transaction aç, siparişi ekle, kalemi ekle, stoğu düş, commit et. Sipariş numarası için bir sequence kullanıyoruz:
CREATE SEQUENCE dbo.OrderNumbers AS int START WITH 1;
GO
CREATE OR ALTER PROCEDURE dbo.PlaceOrderNaive
@CustomerId int, @BookId int, @Quantity smallint
AS
BEGIN TRANSACTION;
INSERT dbo.Orders (OrderNumber, CustomerId, Status, CreatedAt, TotalAmount, Currency)
VALUES (CONCAT('BK-N', NEXT VALUE FOR dbo.OrderNumbers), @CustomerId, 'Pending', SYSUTCDATETIME(), 0, 'TRY');
DECLARE @OrderId int = SCOPE_IDENTITY();
INSERT dbo.OrderItems (OrderId, BookId, Quantity, UnitPrice)
SELECT @OrderId, BookId, @Quantity, Price FROM dbo.Books WHERE BookId = @BookId;
UPDATE dbo.StockItems SET QuantityOnHand -= @Quantity WHERE BookId = @BookId;
COMMIT;
2 numaralı kitabın stoğu 140. Müşteri 500 adet istiyor. StockItems'ta stoğun negatife düşmesini engelleyen bir CHECK constraint var. Procedure'ü çalıştırınca hata alacağımız kesin; soru şu: hatadan sonra veritabanında bu sipariş var mı?
EXEC dbo.PlaceOrderNaive @CustomerId = 42, @BookId = 2, @Quantity = 500;
(1 rows affected)
(1 rows affected)
Msg 547, Level 16, State 1, Server b8be943222fa, Procedure dbo.PlaceOrderNaive, Line 10
The UPDATE statement conflicted with the CHECK constraint "CK_StockItems_Quantity". The conflict occurred in database "sqls_14", table "dbo.StockItems".
The statement has been terminated.
OrderId|OrderNumber|items
-------|-----------|-----
1000001|BK-N1|1
Sipariş ve kalemi kaydedildi, stok düşülmedi. Transaction vardı, hata vardı, ama SQL Server bu hatada transaction'ı geri almadı: 547 statement seviyesinde bir hata, yalnız o UPDATE'i iptal ediyor ve procedure bir sonraki satırla, yani COMMIT ile devam ediyor. İlk iki satırdaki (1 rows affected) mesajları da bu procedure'ün başka bir eksiğini gösteriyor. Bu yazıda stored procedure'ün doğru iskeletini, output parameter ve return code'u, yetki zincirini ve dynamic SQL'i güvenli yazmayı ele alıyoruz.
Doğru İskelet: NOCOUNT, XACT_ABORT, TRY/CATCH ve THROW
Aynı işi Bookstore'un uygulamaya açtığı api schema'sında, production'a koyacağımız şekilde yazıyoruz:
CREATE SCHEMA api AUTHORIZATION dbo;
GO
CREATE OR ALTER PROCEDURE api.PlaceOrder
@CustomerId int,
@BookId int,
@Quantity smallint,
@OrderId int OUTPUT
AS
BEGIN
SET NOCOUNT ON;
SET XACT_ABORT ON;
IF @Quantity <= 0
THROW 50010, N'Quantity must be positive.', 1;
BEGIN TRY
BEGIN TRANSACTION;
UPDATE dbo.StockItems
SET QuantityOnHand -= @Quantity, UpdatedAt = SYSUTCDATETIME()
WHERE BookId = @BookId AND QuantityOnHand >= @Quantity;
IF @@ROWCOUNT = 0
BEGIN
ROLLBACK;
RETURN 1;
END;
INSERT dbo.Orders (OrderNumber, CustomerId, Status, CreatedAt, TotalAmount, Currency)
VALUES (CONCAT('BK-P', NEXT VALUE FOR dbo.OrderNumbers), @CustomerId, 'Pending', SYSUTCDATETIME(), 0,
CASE WHEN (SELECT Country FROM dbo.Customers WHERE CustomerId = @CustomerId) = 'TR'
THEN 'TRY' ELSE 'EUR' END);
SET @OrderId = SCOPE_IDENTITY();
INSERT dbo.OrderItems (OrderId, BookId, Quantity, UnitPrice)
SELECT @OrderId, BookId, @Quantity, Price FROM dbo.Books WHERE BookId = @BookId;
UPDATE dbo.Orders
SET TotalAmount = (SELECT SUM(Quantity * UnitPrice) FROM dbo.OrderItems WHERE OrderId = @OrderId)
WHERE OrderId = @OrderId;
COMMIT;
RETURN 0;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0 ROLLBACK;
THROW;
END CATCH;
END;
Satır satır gerekçeler:
SET NOCOUNT ON: her statement'ın "rows affected" mesajını (protokoldeDONE_IN_PROC) kapatır. Bu mesajlar istemciye ayrı paketler olarak gider; bazı eski istemciler onları result set sanar.SET XACT_ABORT ON: çalışma zamanı hatalarının çoğunda transaction'ı geri alır ve batch'i durdurur. Açılıştaki 547 artık siparişi yarım bırakamaz. Command timeout gibi istemci tarafında kesilen işlerde de transaction'ın açık kalmasını engeller.- Stok kontrolü önce ve tek statement'ta:
WHERE QuantityOnHand >= @Quantity. ÖnceSELECTile kontrol edip sonra düşmek, 11. yazıdaki yarışa kapı açar. CATCH'te önceROLLBACK, sonra parametresizTHROW: orijinal hatayı numarası, mesajı ve satırıyla çağırana iletir.
Üç senaryoyu çalıştıralım: stok yetmez, stok yeter, müşteri yok:
DECLARE @OrderId int, @rc int;
EXEC @rc = api.PlaceOrder @CustomerId = 42, @BookId = 2, @Quantity = 500, @OrderId = @OrderId OUTPUT;
SELECT @rc AS return_code, @OrderId AS order_id, @@TRANCOUNT AS trancount;
EXEC @rc = api.PlaceOrder @CustomerId = 42, @BookId = 2, @Quantity = 1, @OrderId = @OrderId OUTPUT;
SELECT @rc AS return_code, @OrderId AS order_id, TotalAmount, Currency
FROM dbo.Orders WHERE OrderId = @OrderId;
GO
DECLARE @OrderId int, @rc int;
EXEC @rc = api.PlaceOrder @CustomerId = 999999, @BookId = 2, @Quantity = 1, @OrderId = @OrderId OUTPUT;
GO
SELECT @@TRANCOUNT AS trancount, (SELECT QuantityOnHand FROM dbo.StockItems WHERE BookId = 2) AS stock_book_2;
return_code|order_id|trancount
-----------|--------|---------
1|NULL|0
return_code|order_id|TotalAmount|Currency
-----------|--------|-----------|--------
0|1000002|39.00|TRY
Msg 547, Level 16, State 1, Server b8be943222fa, Procedure api.PlaceOrder, Line 27
The INSERT statement conflicted with the FOREIGN KEY constraint "FK_Orders_Customers". The conflict occurred in database "sqls_14", table "dbo.Customers", column 'CustomerId'.
trancount|stock_book_2
---------|------------
0|139
Stok yetmeyince hata değil, return code 1 döndü; bu beklenen bir iş durumu ve exception olmamalı. Müşteri yokken foreign key hatası olduğu gibi çağırana gitti ama stok düşümü geri alındı: 140'tan 139'a yalnız başarılı sipariş düşürdü. Return code'u yalnız durum bildirmek için kullan (0 başarı, pozitif sayılar bilinen iş sonuçları); veri döndürmek output parameter'ın ya da result set'in işi.
THROW mu RAISERROR mı?
Eski kodda her yerde RAISERROR görürsün. İkisinin farkı, hatadan sonra ne olduğunda:
BEGIN TRANSACTION;
RAISERROR(N'Payment failed for order %d.', 16, 1, 1000002);
SELECT 'after RAISERROR' AS step, @@TRANCOUNT AS trancount;
THROW 50020, N'Payment failed for order 1000002.', 1;
SELECT 'after THROW' AS step;
GO
SELECT 'next batch' AS step, @@TRANCOUNT AS trancount;
ROLLBACK;
Msg 50000, Level 16, State 1, Server b8be943222fa, Line 2
Payment failed for order 1000002.
step|trancount
----|---------
after RAISERROR|1
Msg 50020, Level 16, State 1, Server b8be943222fa, Line 4
Payment failed for order 1000002.
step|trancount
----|---------
next batch|1
RAISERROR hatayı yazdı ve kod devam etti; arkasından bir COMMIT gelse yarım işi kaydederdi. THROW batch'i durdurdu: after THROW hiç çalışmadı. İkisi de transaction'ı kendiliğinden geri almadı; bir sonraki batch'te @@TRANCOUNT hâlâ 1. Bu yüzden XACT_ABORT ON ve CATCH'teki ROLLBACK ayrı ayrı gerekli. Yeni kodda THROW kullan; RAISERROR'u yalnız WITH NOWAIT ile ilerleme mesajı basmak gibi özel durumlarda tut. Transaction'ların motor tarafını serinin 9. yazısında (Transaction'lar ve Transaction Log) görmüştük.
Output Parameter, Return Code ve Dapper
Uygulama tarafında aynı procedure'ü Dapper ile çağırıyoruz. Dapper serisinin stored procedure yazısında output parameter ve return value'yu ayrıntılı görmüştük; burada yalnız iskelet:
public sealed record PlaceOrderResult(int ReturnCode, int? OrderId);
public sealed class OrderGateway(SqlConnection connection)
{
public async Task<PlaceOrderResult> PlaceOrderAsync(int customerId, int bookId, short quantity,
CancellationToken ct = default)
{
DynamicParameters parameters = new();
parameters.Add("CustomerId", customerId, DbType.Int32);
parameters.Add("BookId", bookId, DbType.Int32);
parameters.Add("Quantity", quantity, DbType.Int16);
parameters.Add("OrderId", dbType: DbType.Int32, direction: ParameterDirection.Output);
parameters.Add("ReturnCode", dbType: DbType.Int32, direction: ParameterDirection.ReturnValue);
await connection.ExecuteAsync(new CommandDefinition("api.PlaceOrder", parameters,
commandType: CommandType.StoredProcedure, cancellationToken: ct));
return new PlaceOrderResult(parameters.Get<int>("ReturnCode"), parameters.Get<int?>("OrderId"));
}
}
PlaceOrderResult { ReturnCode = 0, OrderId = 1000005 }
PlaceOrderResult { ReturnCode = 1, OrderId = }
SqlException 50010: Quantity must be positive. (procedure api.PlaceOrder, line 12)
commandType: CommandType.StoredProcedure önemli: SqlClient isteği RPC (Remote Procedure Call) olarak gönderir, parametreler tipli gider ve plan cache'te procedure'ün kendi planı kullanılır. Kendi hata numaraların (50010) SqlException.Number'da gelir; uygulama bunları 400 Bad Request'e, bilinmeyenleri 500'e çevirebilir.
Yetki: Ownership Chaining ve Yalnız EXECUTE
Stored procedure'ün en az konuşulan faydası güvenlik. Uygulama kullanıcısına tablolarda hiçbir yetki vermeden, yalnız procedure'leri çalıştırma yetkisi verebilirsin. Test için login'e ihtiyaç duymayan bir veritabanı kullanıcısı açıyoruz:
CREATE ROLE app_runtime;
GRANT EXECUTE ON SCHEMA::api TO app_runtime;
CREATE USER sqls_14_app WITHOUT LOGIN;
ALTER ROLE app_runtime ADD MEMBER sqls_14_app;
GO
EXECUTE AS USER = 'sqls_14_app';
SELECT TOP (1) OrderId FROM dbo.Orders;
GO
DECLARE @OrderId int, @rc int;
EXEC @rc = api.PlaceOrder @CustomerId = 42, @BookId = 3, @Quantity = 1, @OrderId = @OrderId OUTPUT;
SELECT USER_NAME() AS current_user_name, @rc AS return_code, @OrderId AS order_id;
REVERT;
Msg 229, Level 14, State 5, Server b8be943222fa, Line 2
The SELECT permission was denied on the object 'Orders', database 'sqls_14', schema 'dbo'.
current_user_name|return_code|order_id
-----------------|-----------|--------
sqls_14_app|0|1000004
Kullanıcı Orders'ı okuyamıyor ama aynı tabloya yazan procedure'ü çalıştırabiliyor. Bunu sağlayan ownership chaining: api.PlaceOrder'ın sahibi (schema'nın sahibi dbo) ile eriştiği tabloların sahibi aynıysa, SQL Server zincirin devamında yetki kontrolü yapmaz. Bu, uygulama kullanıcısının ele geçirildiği gün saldırganın SELECT * FROM dbo.Customers çalıştıramayacağı anlamına gelir. Bir ayrıntı: sipariş numarası 1000004, çünkü müşteri bulunamadığında geri alınan INSERT 1000003'ü harcadı. Identity değerleri rollback ile geri gelmez.
Güvenli Dynamic SQL
Sipariş arama ekranı müşteri, isteğe bağlı durum ve kullanıcının seçtiği sıralama kolonuyla çalışıyor. Sıralama kolonu parametre olamaz, çünkü SQL'de identifier'lar parameter'la verilemez. Bu yüzden dynamic SQL yazıyoruz, ama kurallarla: değerler sp_executesql parameter'ı, identifier'lar allowlist'ten geçip QUOTENAME ile:
CREATE OR ALTER PROCEDURE api.SearchOrders
@CustomerId int,
@Status varchar(20) = NULL,
@SortColumn sysname = N'CreatedAt',
@SortDescending bit = 1,
@Top int = 3,
@Debug bit = 0
AS
BEGIN
SET NOCOUNT ON;
IF @SortColumn NOT IN (N'CreatedAt', N'TotalAmount', N'OrderNumber')
THROW 50030, N'Invalid sort column.', 1;
DECLARE @sql nvarchar(max) = N'SELECT TOP (@Top) OrderId, OrderNumber, Status, CreatedAt, TotalAmount
FROM dbo.Orders
WHERE CustomerId = @CustomerId'
+ CASE WHEN @Status IS NOT NULL THEN N' AND Status = @Status' ELSE N'' END
+ N'
ORDER BY ' + QUOTENAME(@SortColumn) + CASE WHEN @SortDescending = 1 THEN N' DESC;' ELSE N' ASC;' END;
IF @Debug = 1 PRINT @sql;
EXEC sys.sp_executesql @sql,
N'@CustomerId int, @Status varchar(20), @Top int',
@CustomerId = @CustomerId, @Status = @Status, @Top = @Top;
END;
EXEC api.SearchOrders @CustomerId = 42, @SortColumn = N'TotalAmount', @Debug = 1;
EXEC api.SearchOrders @CustomerId = 42, @SortColumn = N'CreatedAt; DROP TABLE dbo.Orders; --';
SELECT TOP (@Top) OrderId, OrderNumber, Status, CreatedAt, TotalAmount
FROM dbo.Orders
WHERE CustomerId = @CustomerId
ORDER BY [TotalAmount] DESC;
OrderId|OrderNumber|Status|CreatedAt|TotalAmount
-------|-----------|------|---------|-----------
540544|BK-00540544|Delivered|2025-01-20 18:06:00|269.90
783424|BK-00783424|Delivered|2025-12-24 02:06:00|240.79
67681|BK-00067681|Delivered|2023-04-05 00:00:00|218.87
Msg 50030, Level 16, State 1, Server b8be943222fa, Procedure api.SearchOrders, Line 13
Invalid sort column.
Üretilen SQL'de kullanıcıdan gelen hiçbir değer metin olarak yok; yalnız parameter adları var. @Status verilmediğinde koşul SQL'e hiç eklenmiyor, böylece iki farklı sorgu şekli için iki ayrı ve doğru plan oluşuyor. Allowlist, injection denemesini SQL'e ulaşmadan reddetti. QUOTENAME ikinci savunma hattı: allowlist bir gün gevşetilse bile kolon adını köşeli paranteze alıp içindeki ] karakterini kaçırır. SQL injection'ın kendisini Web Güvenliği serisinin OWASP (Open Worldwide Application Security Project) A05 Injection yazısında anlatmıştık; burada konu, dynamic SQL'in veritabanı içinde nasıl yazılacağı.
Dynamic SQL zinciri kırar: module signing
Aynı procedure'ü uygulama kullanıcısıyla çalıştırınca:
Msg 229, Level 14, State 5, Server b8be943222fa, Line 1
The SELECT permission was denied on the object 'Orders', database 'sqls_14', schema 'dbo'.
sp_executesql ile çalışan metin ayrı bir batch'tir ve ownership chain'in parçası değildir; yetki kontrolü çağıran kullanıcıyla yapılır. Kullanıcıya SELECT vermek bütün kazancı yok eder. İki çözüm var. WITH EXECUTE AS OWNER procedure'ü sahibinin kimliğiyle çalıştırır; basit ama procedure içindeki her şey dbo yetkisiyle çalışır ve USER_NAME() gibi fonksiyonlar artık gerçek kullanıcıyı göstermez. Daha dar olanı module signing: procedure'ü bir sertifikayla imzalar, yalnız o sertifikadan oluşturulan kullanıcıya gereken yetkiyi verirsin:
CREATE CERTIFICATE SearchOrdersSigner
ENCRYPTION BY PASSWORD = '$(CertPassword)'
WITH SUBJECT = 'Signs api.SearchOrders';
ADD SIGNATURE TO api.SearchOrders
BY CERTIFICATE SearchOrdersSigner WITH PASSWORD = '$(CertPassword)';
ALTER CERTIFICATE SearchOrdersSigner REMOVE PRIVATE KEY;
CREATE USER SearchOrdersSigner FROM CERTIFICATE SearchOrdersSigner;
GRANT SELECT ON dbo.Orders TO SearchOrdersSigner;
GO
EXECUTE AS USER = 'sqls_14_app';
EXEC api.SearchOrders @CustomerId = 42;
SELECT TOP (1) OrderId FROM dbo.Orders;
REVERT;
OrderId|OrderNumber|Status|CreatedAt|TotalAmount
-------|-----------|------|---------|-----------
1000004|BK-P4|Pending|2026-11-08 12:06:07|35.69
1000002|BK-P2|Pending|2026-11-08 12:05:57|39.00
1000001|BK-N1|Pending|2026-11-08 12:05:21|.00
Msg 229, Level 14, State 5, Server b8be943222fa, Line 3
The SELECT permission was denied on the object 'Orders', database 'sqls_14', schema 'dbo'.
Procedure çalışıyor, ama kullanıcı tabloya doğrudan yine erişemiyor. $(CertPassword) bir sqlcmd değişkeni; şifreyi script'e yazmadan, çalıştırırken -v CertPassword=... ile veriyoruz. Private key'i silmek, sertifikanın başka bir modülü imzalamak için kullanılmasını engeller. Bedeli şu: procedure'ü ALTER ettiğin anda imza düşer. Biz de denedik, sys.crypt_properties'teki imza sayısı 1'den 0'a indi. Deploy script'in, her procedure değişikliğinden sonra imzayı yeni bir sertifikayla tekrar atmalı.
Proc mu ORM mu? Ve Versiyonlama
"İş mantığı stored procedure'de mi, uygulamada mı olmalı" tartışmasının iki makul cevabı var. ORM (Object-Relational Mapping), yani Entity Framework Core (EF Core) tarafında kalmak: iş mantığı tek dilde, unit test'i kolay, refactoring araçları çalışır, veritabanı değiştirmek mümkün. Procedure tarafında kalmak: veri yanında işlenir ve round trip azalır; yetki procedure seviyesinde verilebilir; DBA (Database Administrator) planı uygulamayı deploy etmeden değiştirebilir. Bookstore gibi bir uygulamada iyi bir orta yol: CRUD (Create, Read, Update, Delete) ve basit sorgular EF Core'da, çok tabloya dokunan ve tutarlılığı kritik birkaç işlem (sipariş oluşturma, stok rezervasyonu) ile dinamik raporlama sorguları procedure'de.
Hangisini seçersen seç, procedure'ler repo'da yaşamalı. İki yaygın yol var. SQL Database Projects, yani Microsoft.Build.Sql SDK'sı (Software Development Kit), şemayı ve procedure'leri .sql dosyaları olarak tutar, derleme zamanında referansları doğrular ve bir DACPAC (Data-tier Application Package) üretir; deploy aracı hedef veritabanıyla farkı hesaplar. EF Core migration'ı kullanıyorsan procedure'ü migrationBuilder.Sql(...) ile bir migration'ın içine koyarsın; basit ama her değişiklik yeni bir migration ve procedure'ün son hâlini görmek için migration'ları taramak gerekir. Hangisi olursa olsun, CREATE OR ALTER script'leri idempotent yapar.
SQL Server 2019/2022'de Farkı
Bu yazıdaki her şey SQL Server 2019 ve 2022'de aynen çalışır: CREATE OR ALTER 2016 SP1'den (Service Pack 1), THROW 2012'den beri var; ownership chaining ve module signing değişmedi. Fark, isteğe bağlı parametreli procedure'lerin planında. Dynamic SQL yerine WHERE (@Status IS NULL OR Status = @Status) yazılan "catch-all" procedure'ler 2019'da tek ve çoğu zaman kötü bir plan alır; çözüm OPTION (RECOMPILE) ya da bu yazıdaki gibi dynamic SQL'dir. SQL Server 2022, compatibility level 160'ta Parameter Sensitive Plan optimization ile çarpık dağılımlı eşitlik parametreleri için birden fazla plan tutabiliyor; 2025 ise compatibility level 170'te bu tür isteğe bağlı parametre kalıpları için optional parameter plan optimization getiriyor. Bunları serinin 7. yazısında (Plan Cache, Parameterization ve Parameter Sniffing) ölçtük. Eski sürümlere de hedef veriyorsan dynamic SQL hâlâ en öngörülebilir yol.
Üretimde Dikkat
Hata numaralarını sözleşme gibi yönet. 50000 üstündeki numaraları (50010, 50030) bir dokümanda tut, uygulama tarafında bir enum'a bağla; mesaj metnine göre karar veren kod, mesaj değiştiğinde kırılır.
Procedure'lerin performansını sorgu bazında izle. sys.dm_exec_procedure_stats her procedure için çalışma sayısını, toplam logical read'leri ve CPU'yu (Central Processing Unit) verir; Query Store ise procedure içindeki her statement'ı ayrı ayrı gösterir. Bir procedure'ün yavaşladığını değil, içindeki hangi statement'ın yavaşladığını bulmak gerekir.
Uygulama kullanıcısını ayır. Bu yazıdaki app_runtime rolü yalnız EXECUTE alıyor; migration'ları çalıştıran kullanıcı ayrı ve DDL (Data Definition Language) yetkili. Güvenlik modelini 17. yazıda bütün olarak kuracağız.
Procedure içinde transaction sahipliğini netleştir. Çağıran zaten bir transaction açmışsa, procedure'deki ROLLBACK çağıranın transaction'ını da geri alır ve @@TRANCOUNT uyuşmazlığı hatası (266) verir. Ya "procedure transaction'ı kendisi yönetir, dışarıdan çağrılmaz" kuralını koy ya da @@TRANCOUNT'a bakıp savepoint kullanan bir kalıp seç.
Sık Düşülen Tuzaklar
Procedure içinde string birleştirerek dynamic SQL kurmak. EXEC('... WHERE Status = ''' + @Status + ''''), procedure kullandığın için güvende olduğunu sanmaktır. Injection, SQL'in nerede birleştirildiğine bakmaz.
sp_ önekiyle procedure adlandırmak. SQL Server sp_ ile başlayan adları önce master'da arar; hem küçük bir maliyet hem de sistem procedure'üyle çakışma riski.
CATCH'te hatayı yutmak. BEGIN CATCH SELECT ERROR_MESSAGE(); END CATCH hatayı result set'e çevirir; uygulama başarılı bir çağrı görür. Hata ya THROW ile yukarı gitmeli ya da bilinçli bir return code'a dönüşmeli.
Output parameter'ı okumayı unutmak ya da erken okumak. ActiveX Data Objects for .NET (ADO.NET) tarafında output ve return value, result set'ler tamamen okunup reader kapatıldıktan sonra dolar. Procedure hem result set hem output döndürüyorsa, reader'ı kapatmadan okunan değer null gelir.
Küçük Bir Deneme
★☆☆ dbo.PlaceOrderNaive'in başına yalnız SET XACT_ABORT ON ekle ve açılıştaki çağrıyı tekrarla. Sipariş oluşuyor mu? @@TRANCOUNT kaç?
★★☆ api.SearchOrders'a @CreatedFrom ve @CreatedTo isteğe bağlı parameter'larını ekle. @Debug = 1 ile üretilen dört farklı sorgu şeklini gör ve plan cache'te kaç ayrı plan oluştuğunu sys.dm_exec_query_stats ile say.
★★★ api.SearchOrders'ı imzalamak yerine WITH EXECUTE AS OWNER ile yeniden yaz. Procedure'ün içine SELECT USER_NAME(), ORIGINAL_LOGIN() ekle ve iki yöntemde ne döndüğünü karşılaştır. Audit için hangisi daha iyi?
Procedure'ler veritabanının içinde çalışan kodun en görünür hâli. Daha az görünür olanları da var: her satır için çağrılan function'lar ve her DML (Data Manipulation Language) statement'ından sonra sessizce çalışan trigger'lar. Bir sonraki yazıda scalar UDF (User-Defined Function) inlining'i, inline ve multi-statement table-valued function'ları ve trigger tuzaklarını ölçüyoruz.