ravendbravendb入门


备注

本节概述了ravendb是什么,以及开发人员为什么要使用它。

它还应该提到ravendb中的任何大型主题,并链接到相关主题。由于ravendb的文档是新的,您可能需要创建这些相关主题的初始版本。

一个简单的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的IDocumentStoreDocumentStore ,它是一个接口及其开箱即用的实现,可以连接到RavenDB实例。这是我们需要用来连接服务器的顶级对象,它RavenDB文档建议它在应用程序中用作单例。

所以我们将继续创建一个,但为了简单起见,我们不会在它周围实现单例包装 - 我们将在程序退出时将其处理掉,以便以干净的方式关闭连接。将以下代码添加到main方法:

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

正如开头所说,我们使用Live Test RavenDB实例,我们使用其地址作为DocumentStoreUrl 属性。我们还指定了默认数据库名称,在本例中为“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服务器,提交所有挂起操作。

按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需要前缀为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> 来查询文档集合。它返回一个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 至少一个名称以输入值开头的PetOwner

删除

我们有两个选项可以执行删除。一种是传递文档标识符,如果我们在内存中没有对象本身但我们确实有标识符并且我们希望防止以其他方式避免到数据库的往返,这很有用。另一方面,显然是传递保存到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的详细说明。