一个简单的 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
}
});
}
这包装起来了。你应该能够通过将代码片段粘贴到控制台应用程序中来查看示例。