[C#] SqlCommand.Parameter | Czy zadziała


(Donpedro43) #1

Witam!

Od pewnego czasu przekopuję internet w poszukiwaniu odpowiedzi na moje pytanie.

Przyjmijmy iż chcemy wykonać Insert do bazy danych (w tym przypadku MS SQL Server) w C#.

Do tego celu użyjemy:

using System.Data.Sql;

...

private void btn_Insert_Click(object sender, EventArgs e)

{

               string queryString = "INSERT INTO tekst(tekst) VALUES (@tekst);";


               SqlConnection sqlcon = new SqlConnection(...);

               SqlCommand sqlcom = new SqlCommand(queryString,sqlcon);

                       sqlcom.Parameters.Add("@tekst", SqlDbType.Text);

                       sqlcom.Parameters["@tekst"].Value = "Hello! I'm Steave! *@#$%^&*()<>:{}~~`|\/?.,;' ";

               (...)

}

Reszta kodu nie ma w tym przypadku znaczenia..

Moje pytanie brzmi:

Czy po dodaniu w powyższy sposób parametru do SqlCommand wartość parametru @tekst będzie automatycznie przeformatowana i poprawnie doda się do tabeli?


(somekind) #2

W jakim sensie "automatycznie przeformatowana"?


(Tomek Matz) #3

Jeśli dobrze rozumiem Twój problem, to jesteś ciekaw, czy ten kod jest odporny na SQL Injection? To odpowiadam - tak jest :). Wszelkie znaki specjalne (np. '), które będziesz przekazywał w sparametryzowanych zapytaniach zostaną odpowiednio obsłużone. Dobrym linkiem na początek będzie ten http://msdn.microsoft.com/en-us/library/ff648339.aspx (choć oczywiście najlepiej jest - wszędzie tam gdzie to tylko możliwe - ograniczać użytkownika przed wprowadzaniem znaków specjalnych dla danego SZBD).

Może prosty przykład, aby pokazać na czym polega problem. W tej swojej przykładowej tabeli dodaj sobie parę rekordów, a potem wykonaj taki kod:

string text = "';delete from tekst;--";

SqlCommand cmd = new SqlCommand("select tekst from tekst where tekst = '" + text + "'", conn);

i zobacz co się stanie z rekordami w tabeli. Następnie ponownie dodaj parę rekordów w tabeli i wykonaj taki kod:

string text = "';delete from tekst;--";

SqlCommand cmd = new SqlCommand("select tekst from tekst where tekst = @tekst", conn);

cmd.Parameters.Add("@tekst", SqlDbType.VarChar);

cmd.Parameters["@tekst"].Value = text;

i zobacz co się stanie z rekordami w tabeli. I jeszcze jedno, jeśli .NETowy string zawiera znaki specjalne, to zamiast dla każdego z nich (np. dla wszystkich ) stosować znak ucieczki, czyli \ możesz użyć znaku @, np.:

sqlcom.Parameters["@tekst"].Value = @"Hello! I'm Steave! *@#$%^&*()<>:{}~~`|\/?.,;' ";

Choć u Ciebie w przykładzie jest tylko jedno \, więc w sumie czy użyjesz @"...", czy \ na jedno wyjdzie :slight_smile:


(Donpedro43) #4

@matzu

Wielkie dzięki. Przykładu nie potrzebuję bo napisalem cala aplikacje i poprzez wyrażenia regularne chciałem obsłużyć wszystkie znaki specjalne jak w przypadku:

Więc teraz pozostaje mi tylko jedno... Przerobić wszystko na Insert z parametrami. :smiley:

Dzięki wielkie !

Pozdrawiam

Don_Pedro43


(Tomek Matz) #5

Dokładnie tak. Generalnie jeśli używasz ADO .NET, to powinieneś unikać sklejania zapytań SQL. A jeśli już je sklejasz to sklejaj tylko i wyłącznie zapytania sparametryzowane.


(Donpedro43) #6

Jak nad tym teraz usiadłem, to nasunęła mi się taka myśl. W swoim programie "zlepiam" po 100 takich insertów i wysyłam do serwera. Jeśli tworzyłbym taką "paczkę" za pomocą SqlCommand, to w jaki sposób mam połączyć 100 takich stringów. Można wyciągnąć z SqlCommand queryString i jego z konkatenować? :shock:


(Tomek Matz) #7

Z SqlCommand wyciągniesz tylko to co do niego przypisałeś (właściwość CommandText). Nie wydobędziesz zapytania SQL w takiej postaci, w jakiej zostanie ono wykonane na bazie danych. Zresztą nie potrzebujesz tego do niczego. Jeśli chciałbyś przeprowadzić "batch update" to możesz sklejać sparametryzowane zapytania poprzez oddzielanie ich średnikami. Sęk w tym, że nie wiem, czy to rozwiązanie się sprawdzi. Ja tak nigdy nie robiłem. Możesz się pobawić i zobaczyć, czy wydajność faktycznie wzrośnie (daj znać jeśli tak będzie). Wydaje mi się, że jakiś odczuwalnych różnic nie zauważysz, bo i tak przecież każda instrukcja wykonywana jest pojedynczo.

To co na pewno podniesie wydajność, to zamiana sparametryzowanych zapytań przechowywanych w kodzie na procedury składowane przechowywane w bazie danych. Wzrost wydajności następuje dzięki temu, że nie jest konieczne parsowanie za każdym razem danego zapytania SQL. Sposób obsługi takich procedur składowanych w kodzie jest praktycznie identyczny jak sparametryzowanych zapytań SQL. W miejsce CommandText przypisujesz po prostu nazwę procedury (zamiast zapytania SQL) i ustawiasz CommandType na StoredProcedure (zamiast Text).

I jeszcze jedna (w sumie najważniejsza) rzecz ... Jeśli masz do wykonania za jednym zamachem 100 instrukcji SQL to powinieneś je puszczać w obrębie jednej transakcji (SqlTransaction). Czyli otwierasz połączenie z bazą danych, rozpoczynasz transakcję, w pętli wykonujesz wszystkie instrukcje SQL, zatwierdzasz zmiany w bazie danych jeśli nie wystąpił żaden wyjątek lub cofasz zmiany jeśli w trakcie wykonywania jakiejkolwiek instrukcji SQL wystąpił wyjątek. Nie jest to wprawdzie "batch update", ale powoduje niejako związanie poszczególnych instrukcji SQL ze sobą.


(Donpedro43) #8

Właśnie! Czy jeśli zrobię coś takiego:

SqlConnection sqlcon = new SqlConnection(...);

               string queryString = "INSERT INTO tekst(tekst) VALUES (@tekst);";

               SqlCommand sqlcom = new SqlCommand(queryString,sqlcon);

                       sqlcom.Parameters.Add("@tekst", SqlDbType.Text);

                       sqlcom.Parameters["@tekst"].Value = "Hello! I'm Steave! *@#$%^&*()<>:{}~~`|\/?.,;' ";

               Paczka_Insertów += sqlcom.CommadText.ToString();

to jaką postać otrzymam w Paczka_Insertów? Przewalidowaną posać Insertu czy tylko "INSERT INTO tekst(tekst) VALUES (@tekst);"?

Tak wiem o tym. Lecz zastanawiam się czy otworzenie połączenia z bazą danych na początku konwersji będzie dobre.Samo wywołanie wiem jest banalne tylko nie wiem czy wywołanie za każdym razem wysłania pojedynczego Insertu za pomocą procedury może przyśpieszyć działanie. Po pierwsze od razu przerzucamy obliczenia na serwer bazy danych. Po drugie w moim przypadku zarówno serwer jak i aplikacja działa na tej samej maszynie. Szczerze mówiąc wydajność będzie gigantyczna gdyż (będzie to działać na maszynie A'la Mainframe) i póki testuje u siebie na kompie to widzę różnicę.

Co do SqlTransaction nie mam zbyt dużego doświadczenia, ale poczytam w tym temacie. Mój program przetwarza plik XML na bazę danych. Plik XML o rozmiarze 435 MB to ok 280tys rekordów. :lol: A głównym celem jest plik 280GB na bazę danych... :lol:


(Tomek Matz) #9

AD 1. Już to pisałem, ale napiszę jeszcze raz :slight_smile: Otrzymasz taką postać jaką przypisałeś w konstruktorze SqlCommand, czyli wartość zmiennej queryString.

Dodam tylko dwie uwagi.

  • Jeśli masz zamiar łączyć tak dużą ilość string-ów, to musisz użyć klasy StringBuilder (lepsza wydajność).

  • CommadText.ToString() jest błędne, wystarczy CommadText. W końcu ta właściwość zwraca wartość typu string.

AD 2. Nie każę Ci otwierać połączenia na początku konwersji (cokolwiek masz na myśli). Połączenie masz otworzyć w momencie, gdy będziesz miał już wszystko przygotowane do wysłania do bazy danych. Czyli otwierasz połączenie z bazą danych, w pętli uruchamiasz procedury składowane, które wykonują pojedyncze insert-y, zamykasz połączenie z bazą danych. I jeszcze jedno ... widzisz różnicę, ale w porównaniu jakiego rozwiązania do jakiego? Jak duża jest to różnica (mam na myśli oczywiście czas przetwarzania)? Czy możesz pokazać przykładowy fragment kodu jaki użyłeś dla obu rozwiązań?

AD 3.

Nie jest to żadna tajemna wiedza. Myślę, że przykład na stronie MSDN stanowi dobry punkt startowy SqlTransaction :slight_smile:

Wcześniej o tym nie pisałeś. A sprawdzałeś ten link Examples of Bulk Importing and Exporting XML Documents? Może wcale nie potrzeba pisać osobnej aplikacji, a wystarczy napisać odpowiednie zapytanie odpytujące ten plik XML.

Nie mam żadnych informacji na temat struktury tego pliku, ale możliwe że transakcja jednak nie jest potrzebna (musisz to po prostu przemyśleć). Zawsze możesz napisać ten program tak, że będzie pamiętał (w jakimś logu), w którym miejscu pliku nastąpiło nieudane wstawienie rekordu do bazy danych. Dzięki temu, będzie można wrócić od razu do tego miejsca przy wznowieniu przetwarzania. Istotne jest też to, że nie możesz wczytywać od razu całego pliku XML do pamięci (280 GB to całkiem sporo). Prawdopodobnie będziesz musiał napisać własny parser, ale najlepiej najpierw sprawdź link, który podałem wyżej lub poszukaj, czy jest gotowe rozwiązanie, które umożliwia przetwarzanie pliku XML partiami.


(Donpedro43) #10

I tak właśnie robię po przygotowaniu tzw "paczki" danych. Poprzednio źle Cie zrozumiałem, że po utworzeniu takiego SqlCommand to od razu każdorazowo wysyłam go do bazy danych. :slight_smile:

Co do różnicy chodziło mi tutaj o różnicę czasów tworzenia "paczki", bo uruchamiałem ten sam program na wielu komputerach i z serwerem zdalnym i lokalnym.

W swoim programie miałem kilkanaście bardzo rozbudowanych if(...){...} else if(), które zamieniłem na switch i działa szybciej niż w poprzednim rozwiązaniem.

Wiem wiem... póki co muszę znowu przy tym przysiąść. Teraz tylko praca-dom-praca-dom-studia...

Heh... wiem szukałem takiego rozwiązania, ale w moim przypadku jest to moją pracą inżynierską :smiley: Problem polega na tym ze większość programów nie radzi sobie z rozczytaniem tak dużych plików... I nie poprawnie generują pliki [*.sql] w których znajdują się dane Inserty lub w ogóle nie ruszają z działaniem. :wink:

Nie nie wczytuje całego pliku XML do pamięci. Czytam go wiersz po wierszu. sam mechanizm działa poprawnie. W całej strukturze używam również bloków try/catch i kiedy wystąpi wyjątek jest on przechwytywany i również umieszczany w odpowiedniej Tabeli. Planuję również napisanie mechanizmu wznowienia konwertowania, ale do tego jeszcze póki co daleka droga i brak czasu :wink:

Dzięki wielkie za zaangażowanie w moim wątku. Teraz muszę poważnie przeanalizować strukturę swojego programu.