ravendbRozpoczęcie pracy z Ravendb


Uwagi

Ta sekcja zawiera przegląd tego, czym jest ravendb i dlaczego deweloper może chcieć go użyć.

Powinien również wymieniać wszelkie duże tematy w ravendb i zawierać linki do powiązanych tematów. Ponieważ Dokumentacja dla ravendb jest nowa, może być konieczne utworzenie początkowych wersji tych pokrewnych tematów.

Prosta aplikacja konsoli RavenDB

W tym przykładzie użyjemy instancji Live Test RavenDB .

Zbudujemy tutaj prostą aplikację konsoli, która demonstruje najbardziej podstawowe operacje:

  • kreacja
  • Wyszukiwanie przez Id
  • Zapytanie
  • Aktualizacja
  • Usunięcie

Rozpocznij od utworzenia nowego rozwiązania Visual Studio i dodaj do niego projekt aplikacji konsoli. Nazwijmy to RavenDBDemoConsole. Korzystanie z biblioteki klienta powinno być podobne, jeśli Reader używa VS Code lub jego ulubionego edytora.

Następnie musimy dodać wymagane referencje. Kliknij prawym przyciskiem myszy węzeł References w okienku Solution Explorer i wybierz Manage NuGet. Przeglądaj online w poszukiwaniu „RavenDb.Client”. Będę używał najnowszej stabilnej wersji, która - w chwili pisania tego tekstu - 3.5.2.

Napiszmy kod, prawda? Rozpocznij od dodania następujących instrukcji używania:

using Raven.Client;
using Raven.Client.Document;
 

Pozwalają nam używać IDocumentStore i DocumentStore IDocumentStore , który jest interfejsem i jego gotową implementacją do łączenia się z instancją RavenDB. Jest to obiekt najwyższego poziomu, którego musimy użyć, aby połączyć się z serwerem, a dokumentacja RavenDB wskazuje , że jest on używany jako singleton w aplikacji.

Tak więc pójdziemy naprzód i stworzymy jeden, ale dla uproszczenia nie zaimplementujemy wokół niego opakowania singletona - po prostu wyrzucimy go, gdy program zakończy działanie, więc połączenie zostanie zamknięte w czysty sposób. Dodaj następujący kod do swojej głównej metody:

using (IDocumentStore store = new DocumentStore
{
    Url = "http://live-test.ravendb.net",
    DefaultDatabase = "Pets"
})
{
    store.Initialize();
}
 

Jak powiedziano na początku, używamy wystąpienia Live Test RavenDB, używamy jego adresu jako właściwości Url w DocumentStore . Podajemy również domyślną nazwę bazy danych, w tym przypadku „Zwierzęta”. Jeśli baza danych jeszcze nie istnieje, RavenDB tworzy ją podczas próby uzyskania do niej dostępu. Jeśli istnieje, klient może użyć istniejącego. Musimy wywołać metodę Initialize() , abyśmy mogli zacząć na niej działać.

W tej prostej aplikacji utrzymamy właścicieli i zwierzęta domowe. Myślimy o ich związku, ponieważ jeden właściciel może mieć dowolną liczbę zwierząt, ale jedno zwierzę może mieć tylko jednego właściciela. Chociaż w prawdziwym świecie jedno zwierzę może mieć dowolną liczbę właścicieli, na przykład męża i żonę, zdecydujemy się na to założenie, ponieważ relacje wiele do wielu w bazie danych dokumentów są nieco inaczej traktowane niż w przypadku relacyjna baza danych i zasługuje na własny temat. Wybrałem tę domenę, ponieważ jest wystarczająco powszechna, aby ją zrozumieć.

Powinniśmy teraz zdefiniować nasze typy obiektów domeny:

public class Owner
{
    public Owner()
    {
        Pets = new List<Pet>();
    }

    public string Id { get; set; }
    public string Name { get; set; }
    public List<Pet> Pets { get; set; }

    public override string ToString()
    {
        return
            "Owner's Id: " + Id + "\n" +
            "Owner's name: " + Name + "\n" +
            "Pets:\n\t" +
            string.Join("\n\t", Pets.Select(p => p.ToString()));
    }
}

public class Pet
{
    public string Color { get; set; }
    public string Name { get; set; }
    public string Race { get; set; }

    public override string ToString()
    {
        return string.Format("{0}, a {1} {2}", Name, Color, Race);
    }
}
 

Należy tutaj zwrócić uwagę na kilka rzeczy:

Po pierwsze, nasz Owner może zawierać zero lub więcej Pet . Zauważ, że klasa Owner ma właściwość o nazwie Id podczas gdy klasa Pet nie. Wynika to z faktu, że obiekty Pet będą przechowywane w obiektach Owner , co dość różni się od sposobu, w jaki ten rodzaj relacji zostałby wdrożony w relacyjnej bazie danych.

Można argumentować, że nie należy tego wdrażać w ten sposób - i może być słuszne, to naprawdę zależy od wymagań. Zasadniczo, jeśli Pet ma sens istnieć bez Owner nie powinno się go osadzać, ale istnieć samo z własnym identyfikatorem. W naszej aplikacji zakładamy, że Pet jest uważane za zwierzę Pet tylko wtedy, gdy ma właściciela, w przeciwnym razie byłoby to zwierzę domowe lub bestia. Z tego powodu nie dodajemy właściwości Id do klasy Pet .

Po drugie, zauważ, że identyfikator klasy właściciela jest ciągiem, jak to ogólnie pokazano w przykładach w dokumentacji RavenDB. Wielu programistów przyzwyczajonych do relacyjnych baz danych może uznać to za złą praktykę, co zwykle ma sens w świecie relacyjnym. Ponieważ jednak RavenDB korzysta z Lucene.Net do wykonywania swoich zadań, a ponieważ Lucene.Net specjalizuje się w operowaniu ciągami znaków, jest to w pełni dopuszczalne - również mamy do czynienia z bazą danych dokumentów, która przechowuje JSON, a w końcu wszystko jest reprezentowane jako ciąg w JSON.

Jeszcze jedną rzeczą wartą uwagi na temat właściwości Id jest to, że nie jest to obowiązkowe. W rzeczywistości RavenDB dołącza własne metadane do każdego zapisywanego dokumentu, więc nawet gdybyśmy go nie zdefiniowali, RavenDB nie miałby problemów z naszymi obiektami. Jest jednak ogólnie definiowany dla łatwiejszego dostępu.

Zanim zobaczymy, jak możemy użyć RavenDB z naszego kodu, zdefiniujmy kilka typowych metod pomocniczych. Powinny być zrozumiałe.

// Returns the entered string if it is not empty, otherwise, keeps asking for it.
private static string ReadNotEmptyString(string message)
{
    Console.WriteLine(message);
    string res;
    do
    {
        res = Console.ReadLine().Trim();
        if (res == string.Empty)
        {
            Console.WriteLine("Entered value cannot be empty.");
        }
    } while (res == string.Empty);

    return res;
}

// Will use this to prevent text from being cleared before we've read it.
private static void PressAnyKeyToContinue()
{
    Console.WriteLine();
    Console.WriteLine("Press any key to continue.");
    Console.ReadKey();
}

// Prepends the 'owners/' prefix to the id if it is not present (more on it later)
private static string NormalizeOwnerId(string id)
{
    if (!id.ToLower().StartsWith("owners/"))
    {
        id = "owners/" + id;
    }

    return id;
}

// Displays the menu
private static void DisplayMenu()
{
    Console.WriteLine("Select a command");
    Console.WriteLine("C - Create an owner with pets");
    Console.WriteLine("G - Get an owner with its pets by Owner Id");
    Console.WriteLine("N - Query owners whose name starts with...");
    Console.WriteLine("P - Query owners who have a pet whose name starts with...");
    Console.WriteLine("R - Rename an owner by Id");
    Console.WriteLine("D - Delete an owner by Id");
    Console.WriteLine();
}
 

A nasza główna metoda:

private static void Main(string[] args)
{
    using (IDocumentStore store = new DocumentStore
    {
        Url = "http://live-test.ravendb.net", 
        DefaultDatabase = "Pets"
    })
    {
        store.Initialize();

        string command;
        do
        {
            Console.Clear();
            DisplayMenu();

            command = Console.ReadLine().ToUpper();
            switch (command)
            {
                case "C":
                    Creation(store);
                    break;
                case "G":
                    GetOwnerById(store);
                    break;
                case "N":
                    QueryOwnersByName(store);
                    break;
                case "P":
                    QueryOwnersByPetsName(store);
                    break;
                case "R":
                    RenameOwnerById(store);
                    break;
                case "D":
                    DeleteOwnerById(store);
                    break;
                case "Q":
                    break;
                default:
                    Console.WriteLine("Unknown command.");
                    break;
            }
        } while (command != "Q");
    }
}
 

kreacja

Zobaczmy, jak możemy zapisać niektóre obiekty w RavenDB. Zdefiniujmy następujące typowe metody:

private static Owner CreateOwner()
{
    string name = ReadNotEmptyString("Enter the owner's name.");

    return new Owner { Name = name };
}

private static Pet CreatePet()
{
    string name = ReadNotEmptyString("Enter the name of the pet.");
    string race = ReadNotEmptyString("Enter the race of the pet.");
    string color = ReadNotEmptyString("Enter the color of the pet.");

    return new Pet
    {
        Color = color,
        Race = race,
        Name = name
    };
}

private static void Creation(IDocumentStore store)
{
    Owner owner = CreateOwner();
    Console.WriteLine(
        "Do you want to create a pet and assign it to {0}? (Y/y: yes, anything else: no)", 
        owner.Name);

    bool createPets = Console.ReadLine().ToLower() == "y";
    do
    {
        owner.Pets.Add(CreatePet());

        Console.WriteLine("Do you want to create a pet and assign it to {0}?", owner.Name);
        createPets = Console.ReadLine().ToLower() == "y";
    } while (createPets);

    using (IDocumentSession session = store.OpenSession())
    {
        session.Store(owner);
        session.SaveChanges();
    }
}
 

Zobaczmy teraz, jak to działa. Zdefiniowaliśmy prostą logikę w języku C #, aby tworzyć obiekty Owner oraz tworzyć i przypisywać do nich Obiekty Pet domowych, dopóki użytkownik tego nie zechce. Część, w której dotyczy RavenDB, a więc i w tym artykule skupia się na tym, jak zapisujemy obiekty.

Aby zapisać nowo utworzonego Owner wraz z jego Pet , najpierw musimy otworzyć sesję, która implementuje IDocumentSession . Możemy go utworzyć, wywołując OpenSession na obiekcie magazynu dokumentów.

Należy więc zauważyć różnicę, podczas gdy magazyn dokumentów jest obiektem stałym, który ogólnie istnieje przez cały okres istnienia aplikacji, natomiast IDocumentSession jest krótkotrwałym, lekkim obiektem. Reprezentuje szereg operacji, które chcemy wykonać za jednym razem (lub przynajmniej za pomocą kilku wywołań do bazy danych).

RavenDB podkreśla (i w pewnym stopniu wymusza) unikanie nadmiernej liczby objazdów do serwera, co nazywają „ochroną rozmówców między klientem a serwerem” na stronie internetowej RavenDB. Z tego właśnie powodu sesja ma domyślny limit liczby wywołań bazy danych, które będzie tolerować, dlatego należy zwrócić uwagę na to, kiedy sesja jest otwierana i usuwana. Ponieważ w tym przykładzie traktujemy stworzenie Owner i jego Pet jako operację, która powinna zostać wykonana samodzielnie, robimy to w jednej sesji, a następnie usuwamy.

Widzimy dwa kolejne wywołania metod, które nas interesują:

  • session.Store(owner) , który rejestruje obiekt do zapisania i dodatkowo ustawia właściwość Id obiektu, jeśli nie jest on jeszcze ustawiony. Fakt, że właściwość identyfikatora nazywa się Id jest zatem konwencją.
  • session.Savehanges() wysyła rzeczywiste operacje do wykonania na serwer RavenDB, zatwierdzając wszystkie oczekujące operacje.

Wyszukiwanie przez Id

Inną częstą operacją jest uzyskanie obiektu za pomocą jego identyfikatora. W świecie relacyjnym zwykle robimy to za pomocą wyrażenia Where , określając identyfikator. Ponieważ jednak w RavenDB każde zapytanie jest wykonywane przy użyciu indeksów, które mogą być nieaktualne , nie jest to podejście do przyjęcia - w rzeczywistości RavenDB zgłasza wyjątek, jeśli spróbujemy wykonać zapytanie według identyfikatora. Zamiast tego powinniśmy użyć metody Load<T> , określając identyfikator. Gdy nasza logika menu jest już na miejscu, musimy jedynie zdefiniować metodę, która faktycznie ładuje żądane dane i wyświetla jego szczegóły:

private static void GetOwnerById(IDocumentStore store)
{
    Owner owner;
    string id = NormalizeOwnerId(ReadNotEmptyString("Enter the Id of the owner to display."));

    using (IDocumentSession session = store.OpenSession())
    {
        owner = session.Load<Owner>(id);
    }

    if (owner == null)
    {
        Console.WriteLine("Owner not found.");
    }
    else
    {
        Console.WriteLine(owner);
    }

    PressAnyKeyToContinue();
}
 

Wszystko, co jest tutaj związane z RavenDB, to po raz kolejny inicjalizacja sesji, a następnie użycie metody Load . Biblioteka klienta RavenDB zwróci deserializowany obiekt jako typ, który określamy jako parametr type. Ważne jest, aby wiedzieć, że RavenDB nie wymusza tutaj żadnej kompatybilności - wszystkie właściwości możliwe do zmapowania zostają zmapowane, a właściwości nieodmapowalne.

RavenDB potrzebuje prefiksu typu dokumentu dołączonego do Id - to jest powód wywołania NormalizeOwnerId . Jeśli dokument o określonym identyfikatorze nie istnieje, zwracana jest null .

Zapytanie

Zobaczymy dwa typy zapytań: jedno, w którym sprawdzamy własne właściwości dokumentów Owner i jedno, w którym sprawdzamy osadzone obiekty Pet .

Zacznijmy od prostszego, w którym przeszukujemy dokumenty Owner których właściwość Name zaczyna się od określonego ciągu.

private static void QueryOwnersByName(IDocumentStore store)
{
    string namePart = ReadNotEmptyString("Enter a name to filter by.");

    List<Owner> result;
    using (IDocumentSession session = store.OpenSession())
    {
        result = session.Query<Owner>()
           .Where(ow => ow.Name.StartsWith(namePart))
           .Take(10)
           .ToList();
    }

    if (result.Count > 0)
    {
        result.ForEach(ow => Console.WriteLine(ow));
    }
    else
    {
        Console.WriteLine("No matches.");
    }
    PressAnyKeyToContinue();
}
 

Jeszcze raz, ponieważ chcielibyśmy wykonać zapytanie jako niezależną pracę, otwieramy sesję. Możemy przeszukiwać kolekcję dokumentów, wywołując Query<TDocumentType> na obiekcie sesji. Zwraca obiekt IRavenQueryable<TDocumentType> , na którym możemy wywoływać zwykłe metody LINQ, a także niektóre rozszerzenia specyficzne dla RavenDB. Wykonujemy tutaj proste filtrowanie, a warunkiem jest, że wartość właściwości Name zaczyna się od wprowadzonego ciągu. Bierzemy pierwsze 10 elementów zestawu wyników i tworzymy jego listę. Trzeba zwrócić uwagę na prawidłowe określenie rozmiaru zestawu wyników - oto inne obronne wymuszanie w grze wykonywane przez RavenDB, zwane ochroną zestawu wyników bez ograniczeń. Oznacza to, że (domyślnie) zwracane są tylko pierwsze 128 pozycji.

Nasze drugie zapytanie wygląda następująco:

private static void QueryOwnersByPetsName(IDocumentStore store)
{
    string namePart = ReadNotEmptyString("Enter a name to filter by.");

    List<Owner> result;
    using (IDocumentSession session = store.OpenSession())
    {
       result = session.Query<Owner>()
           .Where(ow => ow.Pets.Any(p => p.Name.StartsWith(namePart)))
           .Take(10)
           .ToList();
    }

    if (result.Count > 0)
    {
        result.ForEach(ow => Console.WriteLine(ow));
    }
    else
    {
        Console.WriteLine("No matches.");
    }
    PressAnyKeyToContinue();
}
 

Ten nie jest dużo bardziej skomplikowany, napisałem go, aby zademonstrować, jak naturalnie możliwe jest zapytanie o właściwości osadzonego obiektu. To zapytanie po prostu zwraca pierwszych 10 Owner którzy mają co najmniej jedno Pet którego imię zaczyna się od wprowadzonej wartości.

Usunięcie

Mamy dwie opcje do usunięcia. Jednym z nich jest przekazanie identyfikatora dokumentu, który jest użyteczny, jeśli nie mamy samego obiektu w pamięci, ale mamy go i chcielibyśmy zapobiec w inny sposób możliwemu do uniknięcia obiegowi bazy danych. Innym sposobem jest oczywiście przekazanie rzeczywistego obiektu zapisanego do RavenDB. Przyjrzymy się pierwszej opcji tutaj, druga wykorzystuje po prostu inne przeciążenie i przekazuje odpowiedni obiekt:

private static void DeleteOwnerById(IDocumentStore store)
{
    string id = NormalizeOwnerId(ReadNotEmptyString("Enter the Id of the owner to delete."));

    using (IDocumentSession session = store.OpenSession())
    {
        session.Delete(id);
        session.SaveChanges();
    }
}
 

Po raz kolejny musimy otworzyć sesję, aby wykonać naszą pracę. Jak wspomniano wcześniej, tutaj usuwamy pożądany obiekt, przekazując jego identyfikator do metody Delete . Prefiks identyfikatora również powinien tu być na miejscu, tak jak miało to miejsce w przypadku metody Load . Aby faktycznie wysłać polecenie usuwania do bazy danych, musimy wywołać metodę SaveChanges , która zrobi to samo, wraz z wszelkimi innymi oczekującymi operacjami zarejestrowanymi w tej samej sesji.

Aktualizacja

Na koniec przyjrzymy się, jak zaktualizować dokumenty. Zasadniczo mamy na to dwa sposoby. Pierwszy jest prosty, ładujemy dokument, w razie potrzeby aktualizujemy jego właściwości, a następnie przekazujemy do metody Store . Powinno to być proste zgodnie z pokazem ładowania i oszczędzania, ale jest kilka rzeczy wartych odnotowania.

Po pierwsze, biblioteka klienta RavenDB wykorzystuje moduł śledzenia zmian, który umożliwia aktualizację dowolnego dokumentu bez przekazywania go do Store dopóki sesja, która załadowała dokument, jest nadal otwarta. W takim przypadku wywołanie funkcji SaveChanges w sesji wystarczy, aby aktualizacja została przeprowadzona.

Po drugie, aby to zadziałało, obiekt oczywiście wymaga ustawienia swojego identyfikatora, aby RavenDB mógł ustalić, co należy zaktualizować.

Powiedziawszy to, przyjrzymy się tylko innemu sposobowi aktualizacji. Istnieje koncepcja zwana łataniem, której można użyć do aktualizacji dokumentów. Podobnie jak w przypadku usuwania, ma również własne scenariusze użycia. Użycie poprzedniej metody do przeprowadzenia aktualizacji jest dobrym sposobem, jeśli mamy już obiekt w pamięci i / lub chcemy użyć jego bezpieczeństwa typu. Używanie łatania jest opcją, jeśli chcemy uniknąć niepotrzebnego obchodzenia bazy danych, jeśli nie mamy jeszcze obiektu w pamięci. Minusem jest to, że tracimy część bezpieczeństwa typu, ponieważ musimy określić właściwości do aktualizacji za pomocą zwykłych ciągów (nic, czego nie potrafiła rozwiązać niektóre magie LINQ). Zobaczmy kod:

private static void RenameOwnerById(IDocumentStore store)
{
    string id = NormalizeOwnerId(ReadNotEmptyString("Enter the Id of the owner to rename."));
    string newName = ReadNotEmptyString("Enter the new name.");

    store.DatabaseCommands.Patch(id, new Raven.Abstractions.Data.PatchRequest[]{
        new Raven.Abstractions.Data.PatchRequest
        {
            Name = "Name",
            Value = newName
        }
    });
}
 

To się kończy. Przykład powinien być widoczny po wklejeniu fragmentów kodu do aplikacji konsoli.

Instalacja lub konfiguracja

Szczegółowe instrukcje dotyczące konfiguracji lub instalacji ravendb.