Przechowywanie dużej ilości danych w SQL Server

SQL Server posiada kilka mechanizmów umożliwiających przechowywanie dużej ilości danych np. plików graficznych.

FileStream

Pierwszych z nich jest FILESTREAM. Przed pojawieniem się typu FILESTREAM, programiści najczęściej zapisywali pliki sami na dysku a lokalizacje przechowali w bazie danych. Rozwiązanie dobre jednak kłopoty powstały w momencie synchronizacji – co jeśli ktoś usunie jakiś plik z dysku? FILESTREAM to nic innego jak zapisanie pliku w bazie danych. W przeciwieństwie do typu varbinary, FILESTREAM przechowywany jest w osobnym pliku a nie w pliku  tabeli. Zwiększa to wydajność, jednak zwykłe poleca się go dla danych większych niż 1 MB.

Najpierw jednak należy skonfigurować odpowiednio bazę danych. Wszystko można zrobić z poziomu T-SQL jednak w poście pokażę jak to wykonać w SQL Server Management Studio. Najpierw klikamy na bazie danych i przechodzimy do zakładki FIleGroups a następnie dodajemy nowy FileStream (FileStreamTest):

image

Następnie musimy podłączyć utworzoną grupę do konkretnego pliku (folderu). Przechodzimy do zakładki Files i tworzymy nowy plik bazy:

image

Jeśli zajrzymy do utworzonego właśnie folderu zobaczymy plik o rozszerzeniu hdr (filestream.hdr) zawierający metadane, $FSLOG służący jako transaction log, oraz kilka folderów których nazwy stanowią globalne identyfikatory – przechowują one już właściwe dane.

Możemy teraz przejść do utworzenia tabeli. Kod:

create table SampleTable
(
ID  uniqueidentifier not null rowguidcol unique,
DATA varbinary(max) FILESTREAM null
)

Warto zauważyć, że tabela z kolumną FileStream musi zawierać unikalny w skali globalnej (rowguidcol) identyfikator. Tabela została utworzona i możemy przejść do umieszczania danych. W momencie gdy usuwamy dane  FILESTREAM tak naprawdę zostaną one permanentnie usunięte z dysku gdy SQL Server odpali pewien proces czyszczący te dane. Spróbujmy dodać dane z poziomu T-SQL:

DECLARE @SomeData AS VARBINARY(MAX)

 -- zaladowanie danych
SELECT @img = CAST(bulkcolumn AS VARBINARY(MAX))

      FROM OPENROWSET(
            BULK
            'C:\obrazek.jpg',
            SINGLE_BLOB ) AS x

           
INSERT INTO Images (ID, Image)
SELECT NEWID(), @img

Z poziomu c#: (odczyt):


SqlCommand sqlCommand = new SqlCommand();
sqlCommand.Connection = con
sqlCommand.CommandText = "SELECT Image FROM Images " + _

                         "WHERE ID = 'jakis_id'"

byte[] buffer= = sqlCommand.ExecuteScalar();

MemoryStream memoryStream = new MemoryStream(buffer);
Bitmap bitmap=new Bitmap(memoryStream);

Na koniec należy wspomnieć również o konfiguracji dostępu do FILESTREAM:

EXEC sp_configure filestream_access_level, 2
GO
RECONFIGURE
GO

Procedura przyjmuje jako parametry liczby 1,2,3 oznaczające kolejno dostęp wyłącznie za pomocą T-SQL, dostęp za pomocą systemu plików oraz dostęp z użyciem sieci (share).

Oczywiście istnieje wiele sposobów zapisu danych – nie tylko przez użycie tablicy bajtów.

Drugim mechanizmem jest czysty typ VARBINARY – bez atrybutu FILESTREAM. Proces wstawania, selekcji danych jest identyczny jednak parametry wydajnościowe są różne. Generalnie zasada jest prosta – dla małych plików wydajniejszy jest czysty VARBINARY ponieważ nie wiąże się on z dodatkowym czasem poświęconym na zapisywanie pliku w innej strukturze. Dla dużych (>1MB) lepiej używać FILESTREAM.

Events vs. Delegates

Zdarzenia oraz delegaty (tak to chyba się tłumaczy?) pełnią podobną funkcje w C#. Jaka jest jednak różnica? Przyjrzyjmy się sposobowi ich użycia:

class Program
{
    public static event EventHandler SimpleEvent = null;
    public static EventHandler SimpleDelegate = null;

    static void Main(string[] args)
    {
        SimpleEvent+=new EventHandler(EventMethod);
        SimpleDelegate += new EventHandler(DelegateMethod);
        
        SimpleEvent(null, null);
        SimpleDelegate(null, null);
    }
    static void DelegateMethod(object sender, EventArgs e)
    {
    }
    static void EventMethod(object sender, EventArgs e)
    {
    }
}

Deklaracja różni się wyłącznie słowem kluczowym. Użycie jest identyczne. Istnieje jednak kilka różnic pomiędzy tymi mechanizmami:

  • Deklaracja w interfejsie

Zdarzenia (tak jak właściwości i metody) mogą być zadeklarowane w interfejsie i potem wymuszone w klasie. W przypadku czystych delagate jest to niemożliwe (interfejs może zawierać wyłącznie właściwości, zdarzenia, metody). Poniższa konstrukcja jest wiec jak najbardziej poprawna:

interface IExample
{
  event EventHandler SampleEvent; 
}

Z kolei poniższa deklaracja nie zostanie skompilowana:

interface IExample
{
  EventHandler SampleEvent; 
}

  • Wywołania

Zdarzenia mogą być wyłącznie wywoływane w klasie, w której zostały zadeklarowane. Niemożliwe jest odpalenie zdarzenia z klasy pochodnej np:

class BaseClass
{
    public event EventHandler SampleEvent=null;
}
class SubClass:BaseClass
{
    private void SomeMethod()
    {
        if(SampleEvent!=null)
            SampleEvent(null,null); // błąd!
    }        
}

W przypadku delegat powyższa konstrukcja jest poprawna.

  • Słowa kluczowe Add \ Remove

Zdarzenia dostarczają dwa nowe słowa kluczowe Add oraz Remove. Są to odpowiedniki dla Get\Set, znanych z właściwości. Przykład (źródło: MSDN):

public class PropertyEventsSample 
{
   private Hashtable eventTable = new Hashtable();

   public event MyDelegate1 Event1 
   {
      add 
      {
         eventTable["Event1"] = (MyDelegate1)eventTable["Event1"] + value;
      }
      remove
      {
         eventTable["Event1"] = (MyDelegate1)eventTable["Event1"] - value; 
      }
   }

   public event MyDelegate1 Event2 
   {
      add 
      {
         eventTable["Event2"] = (MyDelegate1)eventTable["Event2"] + value;
      }
      remove
      {
         eventTable["Event2"] = (MyDelegate1)eventTable["Event2"] - value; 
      }
   }

   public event MyDelegate2 Event3 
   {
      add 
      {
         eventTable["Event3"] = (MyDelegate2)eventTable["Event3"] + value;
      }
      remove
      {
         eventTable["Event3"] = (MyDelegate2)eventTable["Event3"] - value; 
      }
   }

   public event MyDelegate3 Event4 
   {
      add 
      {
         eventTable["Event4"] = (MyDelegate3)eventTable["Event4"] + value;
      }
      remove
      {
         eventTable["Event4"] = (MyDelegate3)eventTable["Event4"] - value; 
      }
   }

   public event MyDelegate3 Event5 
   {
      add 
      {
         eventTable["Event5"] = (MyDelegate3)eventTable["Event5"] + value;
      }
      remove
      {
         eventTable["Event5"] = (MyDelegate3)eventTable["Event5"] - value; 
      }
   }

   public event MyDelegate4 Event6 
   {
      add 
      {
         eventTable["Event6"] = (MyDelegate4)eventTable["Event6"] + value;
      }
      remove
      {
         eventTable["Event6"] = (MyDelegate4)eventTable["Event6"] - value; 
      }
   }
}

  • Sygnatura

Oczywiście zdarzenia mogą mieć dowolną sygnaturę, ale przyjęło się, że powinna wyglądać następująco:

void Name(object sender, EventArgs args)

Argumenty to dowolna klasa dziedzicząca po EventArgs.