一個簡單的 RavenDB 控制檯應用程式
在本例中,我們將使用 Live Test RavenDB 例項 。
我們將在這裡構建一個簡單的控制檯應用程式,演示最基本的操作:
- 建立
- 按 Id 檢索
- 查詢
- 更新
- 刪除
首先建立一個新的 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
s 可以包含零個或多個 Pet
s。請注意,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
s,我們首先需要開啟一個實現 IDocumentSession
的會話。我們可以通過呼叫文件儲存物件上的 OpenSession
來建立一個。
因此,請注意區別,而文件儲存是一個永久性物件,通常在應用程式的整個生命週期中存在,IDocumentSession
是一個短命的輕量級物件。它代表了我們想要一次性執行的一系列操作(或者至少在幾次資料庫呼叫中)。
RavenDB 強調(並且有點強制)你避免過多的往返伺服器,他們稱之為 RavenDB 網站上的“客戶端 - 伺服器聊天保護”。出於這個原因,會話對其可以容忍的資料庫呼叫具有預設限制,因此必須注意何時開啟和處理會話。因為在這個例子中,我們將 Owner
及其 Pet
s 的建立視為一個應該自行執行的操作,我們在一個會話中執行此操作然後我們處理它。
我們可以看到另外兩個我們感興趣的方法呼叫:
session.Store(owner)
,它註冊要儲存的物件,另外,如果尚未設定,則設定物件的Id
屬性。因此,識別符號屬性稱為Id
的事實是一種慣例。session.Savehanges()
將執行的實際操作傳送到 RavenDB 伺服器,提交所有掛起的操作。
按 Id 檢索
另一個常見操作是通過其識別符號獲取物件。在關係世界中,我們通常使用 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 需要 prehuan35 前面的文件型別字首 - 這就是呼叫 NormalizeOwnerId
的原因。如果不存在具有指定 Id 的文件,則返回 null
。
查詢
我們將在這裡看到兩種型別的查詢:一種是我們查詢 Owner
文件的自身屬性,另一種是查詢嵌入的 Pet
物件。
讓我們從更簡單的一個開始,我們在其中查詢 Owner
屬性以指定的字串開頭的 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>
來查詢文件集合。它返回一個 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
s,這些 tehuan45s 至少有一個 Pet
,其名稱以輸入的值開頭。
刪除
我們有兩個選項可以執行刪除。一種是傳遞文件識別符號,如果我們在記憶體中沒有物件本身但我們確實有識別符號並且我們希望防止以其他方式避免到資料庫的往返,這很有用。另一方面,顯然是傳遞儲存到 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
}
});
}
這包裝起來了。你應該能夠通過將程式碼片段貼上到控制檯應用程式中來檢視示例。