Bruke stored procedures i MS SQL med C#

Denne artikkelen gir deg grunnleggende inføring i C# og Stored procedures i SQL 2005. Hvordan lage enkle stored procedures og hvordan kalle disse fra C#.

Stored procedures er ferdig lagrede sql-setninger i SQL Server. Fordelen ved å bruke procedures fremfor SQL i koden er mange, men de viktigste grunnene er sikkerhet og ytelse. Dersom spørringene er store og komplekse vil det også av mange oppfattes mer ryddig å flytte denne delen inn i SQL-server fremfor å ha dette i selve koden.

For å ha litt data å teste med kan vi opprette en tabell vha koden

CREATE TABLE Products
(
   ProductId INT NOT NULL IDENTITY (1, 1),
   ProductName VarChar(255) NOT NULL
)

En helt enkel tabell med 2 kolonner hvorav den ene er ProductId (Primary key) og den andre kan inneholde litt tekst. For å ta ut data fra denne tabellen vil man bruke SELECT og evt sile data vha WHERE. Dersom man vil ha ut et spesifikt produkt vil man feks bruke “SELECT ProductName FROM Products WHERE ProductId=4”. Mange ville da laget følgende kode mot databasen:

string sql = "SELECT * FROM Products WHERE ProductId=" + productId;
SqlConnection conn = new SqlConnection("SERVER=......");
SqlCommand cmd = new SqlCommand(sql, conn);
conn.Open();
SqlDataReader dr = cmd.ExecuteReader();
while (dr.Read())
{
  // hente data.....
}
conn.Close();

Dette er ikke spesielt lurt da det åpner for det som kalles “Sql injections”. Skal ikke gå dypt inn på det, men kort forklart vil man i dette eksempelet kunne sende inn strengen “0; DELETE FROM Products;”. Dette ville med koden i eksempelet gitt setningen “SELECT * FROM Products WHERE ProductId=0; DELETE FROM Products;” Det er ikke noe moro… Så uansett om du bruker stored procedures eller ikke så bør du hvertfall bruke SqlParametre. Det er en langt tryggere måte å sende inn data på. I eksempelet under gjør vi akurat det samme som over, men i stedet for variabler i SQL-setningen bruker vi SqlParameters.

string sql = "SELECT * FROM Products WHERE ProductId=@ProductId";
SqlConnection conn = new SqlConnection("SERVER=......");
SqlCommand cmd = new SqlCommand(sql, conn);
cmd.Parameters.AddWithValue("@ProductId", productId);
conn.Open();
SqlDataReader dr = cmd.ExecuteReader();
while (dr.Read())
{
  // hente data..... 
}
conn.Close();

For å dra eksmpelet enda lengre skal vi opprette en Stored Procedure og kalle den i stedet for å skrive T-SQL i selve koden. For å opprette en enkel prosedyre kan man feks skrive følgende kode:

CREATE PROCEDURE GetProduct
(
  @ProductId Int
)
AS
BEGIN
  SELECT  * FROM Products
  WHERE ProductId=@ProductId
END

Denne enkle prosedyren trenger variabelen @ProductId og vil returnere data om produkt med den angitte ProductId. For å kalle denne prosedyren fra C# kan man bruke følgende kode:

SqlConnection conn = new SqlConnection("SERVER=......");
SqlCommand cmd = new SqlCommand("GetProduct", conn);
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.AddWithValue("@ProductId", productId);
conn.Open();
SqlDataReader dr = cmd.ExecuteReader();
while (dr.Read())
{
  // hente data.....
}
conn.Close();

Koden er nesten lik koden vi brukte tidligere, men det er to ting som er endret: Vi har angitt navnet på prosedyren (GetProducts) i stedet for sql.setningen og vi har angitt egenskapen CommandType i Command objectet til CommandType.StoredProcedure. Koden vil gi eksakt det samme resultatet.

Men det er ved litt mer komplekse database-spørringer at man virkelig ser nytten av prosedyrer. Dersom man har en lang spørring med join’s osv på over 1000 tegn blir det fort veldig rotete og uoversiktlig. En annen viktig fordel er bruken av output-parametre. I eksempelet over sender vi inn en parameter, men disse kan like greit sendes ut fra Sql serveren også.  Sett at man skal lagre et nytt produkt i databasen og man ønsker å få returnert ProductId med det samme. Da kan man bruke en output-paramter.

Først trenger vi en prosedyre som lagrer produkter:

CREATE PROCEDURE AddProduct
(
  @ProductId int OUTPUT,
  @ProductName VarChar(255)
)
AS
BEGIN
  INSERT 
  INTO Products 
    (ProductName)
    VALUES 
    (@ProductName)  
  SET @ProductId=SCOPE_IDENTITY()
END

Denne prosedyren setter inn et product i databasen og tar i bruk funksjonen SCOPE_IDENTITY() for å returnere ProductId til produktet som nettopp ble lagret. SCOPE_IDENTITY() er ny i SQL Server 2005, men man kan bruke @@IDENTITY dersom man bruker SQL Server 2000.

Når vi skal kalle opp denne prosedyren må vi altså sende inn 2 SqlParametre. En som sender inn produktnavnet og en som tar imot produktId.

SqlConnection conn = new SqlConnection("SERVER=......");
SqlCommand cmd = new SqlCommand("AddProduct", conn);
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.AddWithValue("@ProductName", "Mitt nye produkt");


//Legger inn vår output-parameter
cmd.Parameters.AddWithValue("@ProductId", DBNull.Value);
cmd.Parameters["@ProductId"].Direction = ParameterDirection.Output;
cmd.Parameters["@ProductId"].SqlDbType = SqlDbType.Int;
cmd.Parameters["@ProductId"].Size = 4;

//Kjører prosedyren
conn.Open();
cmd.ExecuteNonQuery();
conn.Close();

// Tar ut verdien fra output-parameter
int productId = Convert.ToInt32(cmd.Parameters["@ProductId"].ToString());           

I eksempelet legger vi inn 2 SqlParametre hvor den ene er en output-parameeter. Når man angir en slik parameter sender man inn verdien DBNull.Value og man angir at det er en output-parameter og hvilken datatype og størrelse (Int i SQL er Size 4)

Man kan selvfølgelig sende inn så mange parametre man vil og selve prosedyren kan utføre en rekke oppgaver. Jeg personlig foretrekker å gjøre alle sql-spørringer via Stored Procedures, men det er litt delte meninger om hva som er best. Det er liten tvil om at sikkerheten er bedre ved bruk av SP, men når det kommer til ytelse er det veldig mye uenighet om det er noe å tjene. En stor fordel i tillegg til ytelse er at man kan rette feil i sine sql-setninger uten å endre selve programmet.