本節概述了ravendb是什麼,以及開發人員為什麼要使用它。
它還應該提到ravendb中的任何大型主題,並鏈接到相關主題。由於ravendb的文檔是新的,您可能需要創建這些相關主題的初始版本。
在本例中,我們將使用Live Test RavenDB實例 。
我們將在這裡構建一個簡單的控制台應用程序,演示最基本的操作:
首先創建一個新的Visual Studio解決方案並向其添加一個Console Application項目。我們稱之為RavenDBDemoConsole。如果Reader使用VS Code或他/她喜歡的編輯器,則客戶端庫的用法應該類似。
接下來,我們需要添加所需的引用。右鍵單擊Solution Explorer窗格中的References節點,然後選擇Manage NuGet packages。在線瀏覽'RavenDb.Client'。我將使用最新的穩定版本,在撰寫本文時為3.5.2。
讓我們寫一些代碼,好嗎?首先添加以下using語句:
using Raven.Client;
using Raven.Client.Document;
這些允許我們使用RavenDB的IDocumentStore
和DocumentStore
,它是一個接口及其開箱即用的實現,可以連接到RavenDB實例。這是我們需要用來連接服務器的頂級對象,它的RavenDB文檔建議它在應用程序中用作單例。
所以我們將繼續創建一個,但為了簡單起見,我們不會在它周圍實現單例包裝 - 我們將在程序退出時將其處理掉,以便以乾淨的方式關閉連接。將以下代碼添加到main方法:
using (IDocumentStore store = new DocumentStore
{
Url = "http://live-test.ravendb.net",
DefaultDatabase = "Pets"
})
{
store.Initialize();
}
正如開頭所說,我們使用Live Test RavenDB實例,我們使用其地址作為DocumentStore
的Url
屬性。我們還指定了默認數據庫名稱,在本例中為“Pets”。如果數據庫尚不存在,RavenDB在嘗試訪問它時會創建它。如果確實存在,則客戶端可以使用現有的。我們需要調用Initialize()
方法,以便我們可以開始對它進行操作。
在這個簡單的應用程序中,我們將維護所有者和寵物。我們考慮他們的聯繫,因為一個所有者可能有任意數量的寵物,但一隻寵物可能只有一個所有者。即使在現實世界中,一隻寵物可能擁有任意數量的所有者,例如丈夫和妻子,我們將選擇這種假設,因為文檔數據庫中的多對多關係與關係數據庫,值得擁有自己的主題。我選擇了這個域名,因為它很常見。
所以我們現在應該定義我們的域對像類型:
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);
}
}
這裡有一些注意事項:
首先,我們的Owner
可以包含零個或多個Pet
。請注意, Owner
類有一個名為Id
的屬性,而Pet
類則沒有。這是因為Pet
對象將存儲在 Owner
對像中,這與在關係數據庫中實現這種關係的方式完全不同。
有人可能會爭辯說,這不應該像這樣實施 - 它可能是正確的,它實際上取決於要求。作為一個經驗法則,如果一個Pet
在沒有Owner
情況下存在是有意義的,那麼它不應該被嵌入但是它自己存在並具有自己的標識符。在我們的申請中,我們假設Pet
只有擁有者才被視為寵物,否則它將成為生物或野獸。因此,我們不會向Pet
類添加Id
屬性。
其次,請注意所有者類的標識符是一個字符串,因為它通常顯示在RavenDB文檔的示例中。許多習慣於關係數據庫的開發人員可能認為這是一種不好的做法,這在關係世界中通常是有意義的。但是因為RavenDB使用Lucene.Net來執行它的任務,並且因為Lucene.Net專門用字符串操作它在這裡是完全可以接受的 - 同樣,我們正在處理一個存儲JSON的文檔數據庫,並且,畢竟,基本上所有東西都表示為字符串在JSON中。
關於Id
屬性還有一點需要注意的是它不是強制性的。事實上,RavenDB將自己的元數據附加到我們保存的任何文檔中,因此即使我們沒有定義它,RavenDB也不會對我們的對象產生任何問題。但是,它通常被定義為更容易訪問。
在我們看到如何從代碼中使用RavenDB之前,讓我們定義一些常見的幫助方法。這些應該是不言自明的。
// 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();
}
而我們的主要方法:
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");
}
}
讓我們看看如何將一些對象保存到RavenDB中。讓我們定義以下常用方法:
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();
}
}
現在讓我們看看它是如何工作的。我們已經定義了一些簡單的C#邏輯來創建Owner
對象,並繼續創建和分配Pet
對象,直到用戶需要為止。 RavenDB所涉及的部分因此是本文的重點,是我們如何保存對象。
為了保存新創建的Owner
及其Pet
,我們首先需要打開一個實現IDocumentSession
的會話。我們可以通過調用文檔存儲對像上的OpenSession
來創建一個。
因此,請注意區別,而文檔存儲是一個永久對象,通常在應用程序的整個生命週期中存在, IDocumentSession
是一個短命的輕量級對象。它代表了我們想要一次性執行的一系列操作(或者至少在幾次數據庫調用中)。
RavenDB強調(並且有點強制)你避免過多的往返服務器,他們稱之為RavenDB網站上的“客戶端 - 服務器聊天保護”。出於這個原因,會話對其可以容忍的數據庫調用具有默認限制,因此必須注意何時打開和處理會話。因為在這個例子中,我們將Owner
及其Pet
的創建視為應該自行執行的操作,我們在一個會話中執行此操作然後我們將其處理掉。
我們可以看到另外兩個我們感興趣的方法調用:
session.Store(owner)
,它註冊要保存的對象,另外,設置對象的Id
屬性(如果尚未設置)。因此,標識符屬性被稱為Id
的事實是一種慣例。 session.Savehanges()
將實際操作發送到RavenDB服務器,提交所有掛起操作。 另一個常見操作是通過其標識符獲取對象。在關係世界中,我們通常使用Where
表達式來指定標識符。但是因為在RavenDB中,每個查詢都是使用索引完成的,這可能是陳舊的 ,它不是採取的方法 - 實際上,如果我們嘗試按id查詢,RavenDB會拋出異常。相反,我們應該使用Load<T>
方法,指定id。通過我們的菜單邏輯,我們只需要定義實際加載所請求數據的方法並顯示其詳細信息:
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();
}
這裡與RavenDB相關的所有內容再次是會話的初始化,然後使用Load
方法。 RavenDB客戶端庫將返回反序列化的對像作為我們指定的類型作為類型參數。重要的是要知道RavenDB在這裡不強制執行任何類型的兼容性 - 所有可映射的屬性都被映射而不可映射的屬性不會映射。
RavenDB需要前綴為Id
的文檔類型前綴 - 這就是調用NormalizeOwnerId
的原因。如果不存在具有指定Id的文檔,則返回null
。
我們將在這裡看到兩種類型的查詢:一種是我們查詢Owner
文檔的屬性,另一種是查詢嵌入的Pet
對象。
讓我們從更簡單的一個開始,在其中我們查詢Name
屬性以指定字符串開頭的Owner
文檔。
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();
}
再一次,因為我們想將查詢作為一項獨立工作來執行,我們打開一個會話。我們可以通過在會話對Query<TDocumentType>
上調用Query<TDocumentType>
來查詢文檔集合。它返回一個IRavenQueryable<TDocumentType>
對象,我們可以在其上調用常用的LINQ方法,以及一些特定於RavenDB的擴展。我們在這裡做一個簡單的過濾,條件是Name
屬性的值以輸入的字符串開頭。我們獲取結果集的前10項並創建它的列表。必須注意正確指定結果集大小 - 這是另一個由RavenDB完成的防禦性強制執行,稱為無界結果集保護。這意味著(默認情況下)僅返回前128個項目。
我們的第二個查詢如下所示:
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();
}
這個並不復雜,我編寫它來演示如何自然地查詢嵌入對象屬性。此查詢只返回前10個Owner
至少一個名稱以輸入值開頭的Pet
的Owner
。
我們有兩個選項可以執行刪除。一種是傳遞文檔標識符,如果我們在內存中沒有對象本身但我們確實有標識符並且我們希望防止以其他方式避免到數據庫的往返,這很有用。另一方面,顯然是傳遞保存到RavenDB的實際對象。我們將在這裡查看第一個選項,另一個是使用其他重載並傳遞適當的對象:
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();
}
}
我們再一次需要打開一個會話來完成我們的工作。如前所述,這裡我們通過將其標識符傳遞給Delete
方法來刪除所需的對象。標識符前綴也應該在這裡,就像Load
方法的情況一樣。要將delete命令實際發送到數據庫,我們需要調用SaveChanges
方法,該方法將執行此操作,以及在同一會話中註冊的任何其他掛起操作。
最後,我們將了解如何更新文檔。基本上,我們有兩種方法可以做到這一點。第一個是直截了當的,我們加載文檔,根據需要更新其屬性,然後將其傳遞給Store
方法。根據加載和保存的演示,這應該是直截了當的,但有一些值得注意的事情。
首先,RavenDB客戶端庫使用更改跟踪器,只要加載文檔的會話仍處於打開狀態,就可以更新任何文檔而無需將其實際傳遞給Store
。在這種情況下,在會話上調用SaveChanges
就足以進行更新。
其次,為了使其工作,該對象顯然需要設置其標識符,以便RavenDB可以找出要更新的內容。
有了這些,我們只會看看另一種更新方式。有一個稱為修補的概念,可用於更新文檔。就像刪除它的情況一樣,它也有自己的使用場景。如果我們已經在內存中擁有該對象和/或我們想要使用其類型安全性,則使用先前的方法來執行更新是一種好方法。如果我們想要避免對數據庫進行其他不必要的往返,如果我們在內存中沒有該對象,則使用修補是可選的。缺點是我們失去了一些類型安全性,因為我們必須使用普通字符串指定要更新的屬性(某些LINQ-magic無法解決)。我們來看看代碼:
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
}
});
}
這包裝起來了。您應該能夠通過將代碼片段粘貼到控制台應用程序中來查看示例。
有關設置或安裝ravendb的詳細說明。