środa, 12 września 2012

Dodawanie dużej ilości rekordów do bazy PostgreSql

Zadaniem jest wstawienie 80 tys. rekordów do przykładowej tabeli której strukturę podano poniżej:

CREATE TABLE Table2(
  
Id serial NOT NULL,
  
Name1 CHARacter VARYING(60),
  
Name3 CHARacter VARYING(60),
  
Name2 CHARacter VARYING(60),
  
Name4 CHARacter VARYING(60),
  
CONSTRAINT "Table2_pkey"PRIMARY KEY (Id)
);


Kod tworzący dane testowe:


Code:
    public class Table1
    {
        public int Id { get; set; }
        public string Name1 { get; set; }
        public string Name2 { get; set; }
        public string Name3 { get; set; }
        public string Name4 { get; set; }

        public Table1(string name1, string name2, string name3, string name4)
        {
            Name1 = name1;
            Name2 = name2;
            Name3 = name3;
            Name4 = name4;
        }
    }



Code:
    public class Repository
    {
         public static List<Table1> GetData(int rowCount)
         {
             var data = new List<Table1>();
             for (int i = 0; i < rowCount; i++)
             {
                 data.Add(new Table1(Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(),
                                     Guid.NewGuid().ToString()));
             }

             return data;
         }
    }


Podam tutaj 3 znane mi sposoby, które można użyć w tym celu.

1. Zwykłe inserty:
Standardowy sposób dodawania rekordów, który sprawdza się w przypadku dodania pojedynczego rekordu  do bazy, jednak w przypadku ich dużej ilości powoduje zamulenie całego systemu (co później zobaczymy w wynikach):


Code:
        private static long InsertAsSingleRows(List<Table1> data, NpgsqlConnection connection)
        {
            var stopwatch = new Stopwatch();
            stopwatch.Start();
            using (var cmd = connection.CreateCommand())
            {
                cmd.Parameters.AddWithValue(Name1, "");
                cmd.Parameters.AddWithValue(Name2, "");
                cmd.Parameters.AddWithValue(Name3, "");
                cmd.Parameters.AddWithValue(Name4, "");
                cmd.CommandText = "INSERT INTO Table2(Name1,Name2,Name3,name4) VALUES(@Name1,@Name2,@Name3,@Name4)";
                for (int i = 0; i < data.Count; i++)
                {
                    cmd.Parameters[Name1].Value = data[i].Name1;
                    cmd.Parameters[Name2].Value = data[i].Name2;
                    cmd.Parameters[Name3].Value = data[i].Name3;
                    cmd.Parameters[Name4].Value = data[i].Name4;
                    cmd.ExecuteNonQuery();
                }
            }
            stopwatch.Stop();

            return stopwatch.ElapsedMilliseconds;
        }


2. Transakcja
W przypadku dużej ilości dodawanych rekordów lepiej skorzystać z transakcji, którą na końcu zatwierdzamy:


Code:
        private static long InsertAsTransaction(List<Table1> data, NpgsqlConnection connection)
        {
            var stopwatch = new Stopwatch();
            stopwatch.Start();
            using (var cmd = connection.CreateCommand())
            {
                cmd.Transaction = connection.BeginTransaction();
                cmd.Parameters.AddWithValue(Name1, "");
                cmd.Parameters.AddWithValue(Name2, "");
                cmd.Parameters.AddWithValue(Name3, "");
                cmd.Parameters.AddWithValue(Name4, "");
                cmd.CommandText = "INSERT INTO Table2(Name1,Name2,Name3,name4) VALUES(@Name1,@Name2,@Name3,@Name4)";
                for (int i = 0; i < data.Count; i++)
                {
                    cmd.Parameters[Name1].Value = data[i].Name1;
                    cmd.Parameters[Name2].Value = data[i].Name2;
                    cmd.Parameters[Name3].Value = data[i].Name3;
                    cmd.Parameters[Name4].Value = data[i].Name4;
                    cmd.ExecuteNonQuery();
                }
                cmd.Transaction.Commit();
            }
            stopwatch.Stop();

            return stopwatch.ElapsedMilliseconds;
        }


3. Insert batchowy
Mechanizm ten służy przede wszystkim ładowaniu bardzo dużej ilości rekordów z plików. Jego wydajność jest nieporównywalnie większa niż przedstawionych dwóch poprzednich:


Code:
        private static long InsertAsBatch(List<Table1> data, NpgsqlConnection connection)
        {
            var stopwatch = new Stopwatch();
            stopwatch.Start();
            using (var cmd = connection.CreateCommand())
            {
                var dataAsFile = new StringBuilder();
                for (int i = 0; i < data.Count; i++)
                {
                    dataAsFile.AppendFormat("{0}\t{1}\t{2}\t{3}\n", data[i].Name1, data[i].Name2, data[i].Name3,
                                            data[i].Name4);
                }
                var memoryStream = new MemoryStream(new UTF8Encoding().GetBytes(dataAsFile.ToString()));
                cmd.CommandText = "COPY Table2(Name1,Name2,Name3,name4) FROM STDIN";
                var npgsqlCopyIn = new NpgsqlCopyIn(cmd, connection, memoryStream);
                npgsqlCopyIn.Start();

                stopwatch.Stop();

                return stopwatch.ElapsedMilliseconds;
            }
        }



Czas na wyniki pomiarów czasu:


bardzo dobrze widać to na wykresie:


Podsumowując:
Jeżeli wstawiamy bardzo dużą ilość rekordów warto skorzystać z transakcji. Jeżeli dysponujemy ogromnymi zbiorami danych które chcemy umieścić w bazie możemy skorzystać z inserta batchowego.

wtorek, 28 sierpnia 2012

Mapowanie typów SqLite na typy .Net

Mapując tabelę stworzoną w SqLite na obiekt w .NET, należy pamiętać iż w zależności od typu pola, który został zadeklarowany w SQL zostanie on zmapowany na odpowiednik w .NET. Niektóre typy rodzą nieścisłości jak np. typ INT i INTEGER. Wydawałoby się iż oba te typy są mapowane do zwykłego int-a (Int32) - jednak nie jest tak.

W przypadku korzystania z oficjalnego konektora do bazy System.Data.SQLite (http://system.data.sqlite.org/index.html/doc/trunk/www/downloads.wiki) całą listę mapowania typów można znaleźć w klasie SQLiteConvert.

Dostępna tam lista przedstawia się na obecny czas następująco:

Code:
("COUNTER", DbType.Int64)
("AUTOINCREMENT", DbType.Int64)
("IDENTITY", DbType.Int64)
("LONGTEXT", DbType.String)
("LONGCHAR", DbType.String)
("LONGVARCHAR", DbType.String)
("LONG", DbType.Int64)
("TINYINT", DbType.Byte)
("INTEGER", DbType.Int64)
("INT", DbType.Int32)
("VARCHAR", DbType.String)
("NVARCHAR", DbType.String)
("CHAR", DbType.String)
("NCHAR", DbType.String)
("TEXT", DbType.String)
("NTEXT", DbType.String)
("STRING", DbType.String)
("DOUBLE", DbType.Double)
("FLOAT", DbType.Double)
("REAL", DbType.Double)
("BIT", DbType.Boolean)
("YESNO", DbType.Boolean)
("LOGICAL", DbType.Boolean)
("BOOL", DbType.Boolean)
("BOOLEAN", DbType.Boolean)
("NUMERIC", DbType.Decimal)
("DECIMAL", DbType.Decimal)
("MONEY", DbType.Decimal)
("CURRENCY", DbType.Decimal)
("TIME", DbType.DateTime)
("DATE", DbType.DateTime)
("DATETIME", DbType.DateTime)
("SMALLDATE", DbType.DateTime)
("TIMESTAMP", DbType.DateTime)
("BLOB", DbType.Binary)
("BINARY", DbType.Binary)
("VARBINARY", DbType.Binary)
("IMAGE", DbType.Binary)
("GENERAL", DbType.Binary)
("OLEOBJECT", DbType.Binary)
("GUID", DbType.Guid)
("UNIQUEIDENTIFIER", DbType.Guid)
("MEMO", DbType.String)
("NOTE", DbType.String)
("SMALLINT", DbType.Int16)
("BIGINT", DbType.Int64)

W przypadku korzystania ze zwykłego ADO.NET-a (bez ORMa) znajomość tej listy może ograniczyć ilość rzucanych wyjątków błędnego rzutowania.

Mapowania dla innych baz należy szukać albo w dokumentacji prowidera, albo w dokumentacji (dla MS SQL, Oracle i ODBC mapowiania można znaleźć np tutaj: http://msdn.microsoft.com/en-us/library)/4e5xt97a%28v=vs.110%29.aspx

środa, 15 sierpnia 2012

Pobieranie nazwy właściwości klasy

Za pomocą wyrażeń Lambda Expression można uprościć pobieranie nazwy właściwości w klasie.
Kawałek kodu który może się przydać:


Code:
        public static string GetPropertyName<T>(Expression<Func<T, object>> expression)
        {
            return expression.Body as MemberExpression != null
                ? ((MemberExpression) expression.Body).Member.Name
                : ((MemberExpression) ((UnaryExpression) expression.Body).Operand).Member.Name;
        }

Użycie:

Code:
class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine(GetPropertyName<Person>(person => person.FirstName));
            Console.WriteLine(GetPropertyName<Person>(person => person.LastName));
            Console.WriteLine(GetPropertyName<Person>(person => person.Age));
        }

        public static string GetPropertyName<T>(Expression<Func<T, object>> expression)
        {
            return expression.Body as MemberExpression != null
                ? ((MemberExpression) expression.Body).Member.Name
                : ((MemberExpression) ((UnaryExpression) expression.Body).Operand).Member.Name;
        }
    }

    public class Person
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public int Age { get; set; }
    }

poniedziałek, 13 sierpnia 2012

iTextSharp polskie czcionki

Biblioteka do tworzenia Pdf-ów iTextSharp w swojej podstawowej wersji tworzy Pdf-y bez polskich znaków dialektycznych.

Kod tworzący prosty pdf:


Code:
            var pdf = new Document();
            var output = new FileStream("Plik.pdf", FileMode.Create);
            var pdfWriter = PdfWriter.GetInstance(pdf, output);
            pdf.Open();
            var headerParagraph = new Paragraph("Raport śąóżćń");
            pdf.Add(headerParagraph);
            pdf.Close();
            System.Diagnostics.Process.Start("Plik.pdf");

wygeneruje następującego pdf-a:


Jak widać brakuje tutaj części polskich liter.
Aby użyć polskie litery, należy wskazać czcionkę która ma polskie litery. Możemy zrobić to w ten sposób:


Code:
            var pdf = new Document();
            var output = new FileStream("Plik.pdf", FileMode.Create);
            var pdfWriter = PdfWriter.GetInstance(pdf, output);
            pdf.Open();
            var headerParagraph = new Paragraph("Raport śąóżćń", new Font(BaseFont.CreateFont(@"C:\Windows\Fonts\Arial.ttf", BaseFont.CP1250, true)));
            pdf.Add(headerParagraph);
            pdf.Close();

Na wyjściu otrzymamy dokument z polskimi znakami dialektycznymi:


Innym sposobem jest zarejestrowanie folderu gdzie znajdują się nasze czcionki w FontFactory a następnie pobieranie ich przy pomocy metody GetFont:

Code:
            FontFactory.Register(@"C:\Windows\Fonts");
            FontFactory.GetFont("Arial");

Drugi sposób wydaje mi się bardziej wygodny i użyteczny, z drugiej strony jeżeli czcionki są porozrzucane pomiędzy różne katalogi - sposób pierwszy wydaje się bardziej dogodny.

środa, 8 sierpnia 2012

Indeks kolumny w GridView

Chcąc pobrać indeks kolumny w GridView (ASP.NET WebForms) możemy na trafić na problem, iż w GridView nie zaimplementowano takiej możliwości bezpośrednio.

Jeżeli korzystamy z kolumn, które są bindowane do pól obiektów, możemy posłużyć się właściwością DataField dla kolumny.
Problemem jest to, iż kolumny są typu DataControlField a typ ten nie zawiera właściwości DataField. Klasy dziedziczące po tym typie zawierają bądź też nie zawierają tej właściwości.
W tym przypadku możemy się posłużyć rzutowaniem, bądź też skorzystać z refleksji w celu zmniejszenia ilości warunków:

Code:
        <asp:GridView runat="server" DataSourceID="dsProducts" AutoGenerateColumns="False"
        ID="gvProducts">
            <Columns>
                <asp:BoundField DataField="ProductId" HeaderText="Id"/>
                <asp:BoundField DataField="Name" HeaderText="Nazwa"/>
                <asp:BoundField DataField="Size" HeaderText="Rozmiar"/>
                <asp:DataBoundFieldEx DataField="Weight" HeaderText="Waga" UniqeName="column" />
            </Columns>
        </asp:GridView>

Oraz metoda która pozwala wyciągnąć indeks kolumny po nazwie DataField:

Code:
using System;
using System.Web.UI.WebControls;

namespace WebApplication1
{
    public static class DataGridExtensions
    {
         public static int GetColumnIndexByName(this GridView gridView, string columnName)
         {
             gridView.Columns[]
             if (string.IsNullOrWhiteSpace(columnName))
             {
                 throw new ArgumentNullException("ColumnName can't be null");
             }

             for(int i = 0; i < gridView.Columns.Count; ++i)
             {
                 var dataFieldProperty = gridView.Columns[i].GetType().GetProperty("DataField");
                 if (dataFieldProperty != null)
                 {
                     var column = dataFieldProperty.GetValue(gridView.Columns[i], null).ToString();
                     if (!string.IsNullOrWhiteSpace(column) && column.Equals(columnName))
                     {
                         return i;
                     }
                 }
             }

             return -1;
         }
    }
}

Użycie:

Code:
var columnIndex = gvProducts.GetColumnIndexByName("Name");

niedziela, 5 sierpnia 2012

Oracle Kursory

Kursor w kontekście bazy danych można porównać o pętli, która iteruje po wierszach zwróconych przez zapytanie.

W bazie posiadam tabelę która zawiera dane o klientach:


Deklaracja ogólna kursora:

CURSOR nazwa [(parametry)] [RETURNS] IS selekt;

Przykład dla naszego kursora:

DECLARE
CURSOR myCursor IS
SELECT Imie, Nazwisko FROM klienci;

Po stworzeniu kursora możemy z niego pobrać dane na dwa sposoby:
- Instrukcja Fetch:
set serveroutput on format wraped;
DECLARE
v_pobrany_klient Klienci%ROWTYPE;
CURSOR myCursor IS
SELECT * FROM klienci;
BEGIN
dbms_output.enable;
OPEN myCursor;
LOOP
FETCH myCursor INTO v_pobrany_klient;
dbms_output.put_line(v_pobrany_klient.Imie ||' '|| v_pobrany_klient.Nazwisko);
EXIT WHEN myCursor%NOTFOUND;
END LOOP;
CLOSE myCursor;
END;

- pętla FOR:
set serveroutput on format wraped;
DECLARE
CURSOR myCursor IS
SELECT Imie, Nazwisko FROM klienci;
BEGIN
dbms_output.enable;
FOR row IN myCursor LOOP
  dbms_output.put_line(row.Imie ||' '|| row.Nazwisko);
END LOOP;
END;

Pętla ma tę przewagę nad instrukcją FETCH, że nie trzeba jawnie otwierać i zamykać kursora - prostsze użycie.

środa, 1 sierpnia 2012

MSDTC - Microsoft Distributed Transaction Coordinator

Wielu programistów nie zdaje sobie sprawy z istnienia MSDTC. Czym jest i czy warto o nim wiedzieć podczas tworzenia zapytań? W tym poście postaram się to rozjaśnić.

Jak sama nazwa mówi, MSDTC jest usługą systemu Windows zapewniającą infrastrukturę dla systemów rozproszonych transakcji. Jak wiadomo transakcja ma 4 podstawowe właściwości pochodzące od akronimu ACID:
  • Atomic - atomowość - czyli transakcja wykona się albo w całości albo w ogóle
  • Consistency - spójność - po wykonaniu transakcji nie może dojść do naruszenia jakichkolwiek więzów spójności
  • Isolation - izolacja - w przypadku gdy dwie transakcje wykonują się równocześnie w zależności od poziomu izolacji nie widzą zmian wprowadzany przez siebie nawzajem
  • Durability - trwałość - po zapisaniu transakcji system musi umożliwić uruchomienie się nawet w przypadku awarii
Główni aktorami w transakcji są:
  • Manager transakcji - MSDTC
  • Inicjator - aplikacja zapoczątkowująca transakcję
  • Manager zasobów
Przepływ jest następujący:
  1. Aplikacja inicjująca (Inicjator) wysyła żądanie do managera transakcji o udostępnienie wolnej transakcji
  2. Aplikacja kliencka wysyła żądanie do managera zasobów żądanie o wykonanie zadania jako transakcji
  3. Aplikacja kliencka zatwierdza transakcję
  4. Manager transakcji we współpracy z managerem zasobów sprawdza czy wszystkie zadania zostały wykonane poprawnie - sprawdza w ten sposób zasady ACID