文档数据库:一览






4.97/5 (64投票s)
一览几种文档数据库及其使用方法
引言
我不知道你们中有多少人使用SQL。很多人吧?我确实如此,众所周知它是一个关系型数据库,我们可以在其中存储关系型SQL数据类型,例如INT/CHAR/NVARCHAR等等,我相信你们都知道我在说什么,并且在过去大量使用过关系型数据库。
你认为还有其他类型的数据库吗?没有?实际上,除了关系型数据库之外,还有一些不同类型的数据库,例如
- 平面文件
- 对象
- NoSQL / 文档 / 键值对
现在我不敢说自己对平面文件或对象数据库了解很多,但我花了一些时间评估和了解一些新的文档数据库。在本文中,我选择研究三种不同的文档数据库,并为此文章附带的代码创建了演示。但在我们开始讨论它们以及如何使用它们之前,让我们花一些时间谈谈这些文档数据库日益普及的原因。
我已经说过本文将讨论文档数据库,那么这些文档数据库是什么?以及为什么你可能想使用它们?
使用文档数据库的原因可能来自于许多需求,例如
- 如果你放弃关系模型,可能会更好地表达你的业务模型
- RESTful API(尽管大多数用户会尝试为其首选语言找到原生客户端)
- 模式更改不像关系型数据库那样重要,支持对模式进行即时更改
这些是其中一些原因,那么我们来看看典型的文档数据库可能至少提供的一些功能
- 支持Http的服务器,能够处理标准HTTP数据请求(例如PUT/GET/POST/DELETE),因此如果你的首选语言没有可用的驱动程序,你也可以始终使用标准的Http请求。
- 文档通常以某种可序列化格式存储(最常见的是JSON或BSON)
- 能够存储整个文档。是的,我指的是一个文档,而不是一个带有方法继承层次等丰富对象。这些在文档数据库中不存在;代码不是数据库的一部分。
- 分片
- 复制
在使用文档数据库时,有一点值得注意,那就是“最终一致性”的概念,这与我们在关系型世界中普遍习惯的有所不同。
这个词是什么意思?听起来很可怕,对吧。我想确实有点,但在使用文档数据库时,这似乎是它们都遵循的一种常见方法,允许你将更新/插入推送到文档存储中,但这并不意味着这些更改会立即被所有数据读取者看到。当然,它们最终会被写入所有读取源,但不会立即写入。这意味着我们可能会偶尔看到不一致的情况。
为什么会这样?它是如何产生的?这实际上都与扩展性和可用性有关。如果你只有一个数据源,就像在关系型数据库世界中通常那样,那么在写入时就必须锁定读取。这是一个简单的模型,但它保持完全一致,但扩展性不佳,必须使用某种形式的分片,这在关系型数据库中并不常见。事实上,我从未在关系型数据库中做过分片,所以我不确定它是否甚至可能,也许是,也许不是。
总之,这是我第一时间想提醒你们的一个领域,如果你想了解更多,我发现以下链接在这方面非常有启发性
- http://ayende.com/blog/4447/that-no-sql-thing (Ayende 编写了 RavenDB,所以值得一读)
- http://smoothspan.wordpress.com/2007/12/22/eventual-consistency-is-not-that-scary/
你可能会发现仅凭这一点就足以说明文档数据库可能不适合你,但这只是你自己能做出的决定
无论如何,你不需要太担心这些问题,因为本文将更侧重于文档数据库的基本用法,例如如何执行简单的 CRUD 操作,我只是觉得有必要提前提及,让你有所了解,所以就这样吧,你已经被警告了。
现在市面上有许多文档数据库,我无法一一介绍。在我的初步评估中,我根据我认为最好的属性选择了一些,例如
- 特点
- 易用性
- 声誉
- .NET 驱动可用性(我目前是一名 .NET 开发人员,所以本文是关于如何将 .NET 与相关文档数据库配合使用的)
根据这些属性列表,我得到了一个相当大的列表,我进一步筛选后得到了三个文档数据库,我将在本文中讨论它们。
- Redis
- Raven
- Mongo
不要指望读完本文就能成为文档数据库专家,但我希望在读完本文后,您能够(大致)理解它们的工作原理,并能够继续使用它们并自行找到所需的任何剩余答案。
先决条件
在我们开始之前,您需要确保已下载相关的 NoSQL 服务器和 .NET 客户端 API。我本想随本文上传它们,但不幸的是它们对于 codeproject 的限制来说太大了,所以这项任务必须由您来完成。因此,下面列出了您需要下载的组件
Redis
对于 Redis,您需要下载以下 3 个项目,并确保正确引用了正确的 .NET 部分
服务器
- 可从以下网址下载:https://redis.ac.cn/download
- 确保更改`DocumentDB.Redis`项目内`RedisServer.cs`类顶部的字符串。
.NET 客户端:Service Stack
- 可从以下网址下载:https://github.com/ServiceStack/ServiceStack.Redis
- 下载完成后,请确保修复`DocumentDB.Redis`项目中的所有引用。
Raven
对于 Raven,您需要下载以下 2 个项目,并确保正确引用了正确的 .NET 部分
服务器
- 可从以下网址下载:http://builds.hibernatingrhinos.com/builds/RavenDB
- 确保更改`DocumentDB.Raven`项目内`RavenServer.cs`类顶部的字符串。
.NET 客户端
- .NET 客户端实际上作为上面提到的整体下载的一部分提供
- 下载 Raven 后,请确保修复`DocumentDB.Raven`项目中的所有引用。
MongoDB
对于 MongoDB,您需要下载以下 2 个项目,并确保正确引用了正确的 .NET 部分
服务器
- 可从以下网址下载:https://mongodb.ac.cn/downloads
- 确保更改`DocumentDB.Mongo`项目内`MongoDBServer.cs`类顶部的字符串。
.NET 客户端 (Mongo 支持的客户端)
- 可从以下网址下载:https://github.com/mongodb/mongo-csharp-driver/downloads
- 下载完成后,请确保修复`DocumentDB.Mongo`项目中的所有引用。
重要提示:下载这些项目并妥善放置后,您需要执行以下操作
- 确保本文附带的可下载演示代码中的三个项目,其引用已修复,指向您下载 .NET 客户端 API DLL 的位置,我希望您在执行上述步骤时已经完成了此操作。如果还没有,请务必现在就做。
- 在本文附带的可下载演示代码中的三个项目中,都有一个简单的服务器包装器,它只负责启动正确的实际 NoSQL 服务器进程。这主要是为了方便起见,您需要更改实际 NoSQL 服务器位置的路径,以匹配您下载它的位置。这可以通过更改本文附带的可下载演示代码中三个演示项目中的每个 XXXXServer.cs 类中的字符串值来完成。
顺便说一句,当我在开发本文代码时,我将 NoSQL 服务器/.NET 客户端与可下载的解决方案放在一起,大致如下图所示,但您可以将它们放在您想要的任何位置,只需确保对演示代码中的所有三个项目执行上述两个步骤即可
Redis 文档数据库用法
在本节中,我将讨论使用 Redis。Redis 命令是 Redis 的核心,完整的命令列表可以在 https://redis.ac.cn/commands 找到,这里有一个截图展示了我所说的那种内容
此截图仅显示了可用 Redis 命令的一小部分。
现在,即使 Redis 内部是这样工作的,你也不需要真正了解这些命令,因为任何 Redis 客户端在与服务器通信时都会代表你调用这些命令。我只是觉得向你展示 Redis 的底层工作原理可能有用。
了解 Redis 的另一个非常重要的方面是,它被设计用于处理极度动态的数据集,因此它实际上是希望您的整个数据集都应该适合内存。这听起来可能很疯狂,但这确实取决于您需要编写的应用程序类型。
虽然 Redis 在内存中运行,但它确实有多种磁盘持久化模式,即日志记录和/或完整快照。有关更多信息,请参阅此链接
https://redis.ac.cn/topics/persistence
如果你想要能够随时重新加载到内存中的数年存储,那么 Redis 可能不适合你。另一方面,如果你有一些非常动态、快速移动的数据,并且可以接受在 X 时间后过期,那么 Redis 将非常适合。
服务器
Redis 服务器(可在此处下载:https://redis.ac.cn/download)是用 C++ 编写的,可以使用“`redis-server.exe`”进程运行。事实上,当你下载 Redis 服务器后,你应该会看到类似这样的东西
其中有许多不同的进程可用于管理 Redis 服务器。
.NET 客户端
Redis 有很多适用于各种语言的客户端。本文的演示代码使用 .NET,所以我显然必须选择一个 .NET 客户端,但选择哪一个呢?这取决于我做一些研究并选择一个,ServiceStack 似乎很受欢迎,所以这是我选择的。可以在这里找到它:https://github.com/ServiceStack/ServiceStack.Redis
我几乎不可能概述 Reis 的所有功能,但我将概述我认为在开始使用 Redis 时最重要的部分。
第一步
你必须运行 Redis 实际服务器。我已经尝试通过创建一个名为“`RedisServer`”的帮助类来简化此操作,你应该修改它以指向你的 Redis 服务器下载位置。一旦 Redis 实际服务器运行起来,我们需要创建一个软件`RedisClient`(它连接到实际的 Redis 服务器实例)。
下面显示了一些骨架代码,所有 Redis 代码在附加的演示代码中都使用了这些代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using ServiceStack.Redis;
namespace DocumentDB.Redis
{
public class Program
{
[STAThread]
static void Main(string[] args)
{
Run();
}
public static void Run()
{
try
{
RedisClient redisClient = new RedisClient("localhost");
if (RedisServer.Instance.Start())
{
//Use the software RedisClient which talks to actual Redis server
//Use the software RedisClient which talks to actual Redis server
//Use the software RedisClient which talks to actual Redis server
//Use the software RedisClient which talks to actual Redis server
}
}
catch (Exception ex)
{
Console.WriteLine("============= OH NO : ERROR ============");
}
Console.ReadLine();
}
}
}
这个软件`RedisClient`随后被演示代码中的各种类使用,因此你可以在附加的演示代码中看到`RedisClient`对象的使用。
使用类型化对象的基本 CRUD 操作
为了向您展示如何使用我选择的 Redis 客户端(Service Stack),您真正需要知道的只是如何使用`RedisClient`的实例,它通常按以下方式使用,以获取`IRedisTypedClient`
using (var redisBlogs = redisClient.GetTypedClient<Blog>()) { var blogs = redisBlogs.GetAll(); foreach (Blog blog in blogs) { Console.WriteLine(blog.Dump()); } }
下面列出了各种操作的示例,展示了如何使用 Service Stack `RedisClient`执行各种操作,其中使用了以下文档类来存储
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace DocumentDB.Redis
{
public class Blog
{
public Blog()
{
this.Tags = new List<string>();
this.BlogPostIds = new List<long>();
}
public long Id { get; set; }
public long UserId { get; set; }
public string UserName { get; set; }
public List<string> Tags { get; set; }
public List<long> BlogPostIds { get; set; }
}
public class BlogPost
{
public BlogPost()
{
this.Categories = new List<string>();
this.Tags = new List<string>();
}
public long Id { get; set; }
public long BlogId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public List<string> Categories { get; set; }
public List<string> Tags { get; set; }
}
public class User
{
public User()
{
this.BlogIds = new List<long>();
}
public long Id { get; set; }
public string Name { get; set; }
public List<long> BlogIds { get; set; }
}
}
获取数据
private IList<Blog> Blogs()
{
IList<Blog> blogs = new List<Blog>();
using (var redisBlogs = redisClient.GetTypedClient<Blog>())
{
blogs = redisBlogs.GetAll();
}
return blogs;
}
private IList<User> Users()
{
IList<User> users = new List<User>();
using (var redisUsers = redisClient.GetTypedClient<User>())
{
users = redisUsers.GetAll();
}
return users;
}
插入数据
public void InsertTestData()
{
using (var redisUsers = redisClient.GetTypedClient<User>())
using (var redisBlogs = redisClient.GetTypedClient<Blog>())
using (var redisBlogPosts = redisClient.GetTypedClient<BlogPost>())
{
var ayende = new User { Id = redisUsers.GetNextSequence(), Name = "Oren Eini" };
var mythz = new User { Id = redisUsers.GetNextSequence(), Name = "Demis Bellot" };
var ayendeBlog = new Blog
{
Id = redisBlogs.GetNextSequence(),
UserId = ayende.Id,
UserName = ayende.Name,
Tags = new List<string> { "Architecture", ".NET", "Databases" },
};
var mythzBlog = new Blog
{
Id = redisBlogs.GetNextSequence(),
UserId = mythz.Id,
UserName = mythz.Name,
Tags = new List<string> { "Architecture", ".NET", "Databases" },
};
var blogPosts = new List<BlogPost>
{
new BlogPost
{
Id = redisBlogPosts.GetNextSequence(),
BlogId = ayendeBlog.Id,
Title = "RavenDB",
Categories = new List<string> { "NoSQL", "DocumentDB" },
Tags = new List<string> {"Raven", "NoSQL", "JSON", ".NET"}
},
new BlogPost
{
Id = redisBlogPosts.GetNextSequence(),
BlogId = mythzBlog.Id,
Title = "Redis",
Categories = new List<string> { "NoSQL", "Cache" },
Tags = new List<string> {"Redis", "NoSQL", "Scalability", "Performance"}
},
new BlogPost
{
Id = redisBlogPosts.GetNextSequence(),
BlogId = ayendeBlog.Id,
Title = "Cassandra",
Categories = new List<string> { "NoSQL", "Cluster" },
Tags = new List<string> {"Cassandra", "NoSQL", "Scalability", "Hashing"}
},
new BlogPost
{
Id = redisBlogPosts.GetNextSequence(),
BlogId = mythzBlog.Id,
Title = "Couch Db",
Categories = new List<string> { "NoSQL", "DocumentDB" },
Tags = new List<string> {"CouchDb", "NoSQL", "JSON"}
},
};
ayende.BlogIds.Add(ayendeBlog.Id);
ayendeBlog.BlogPostIds.AddRange(blogPosts.Where(x => x.BlogId == ayendeBlog.Id).ConvertAll(x => x.Id));
mythz.BlogIds.Add(mythzBlog.Id);
mythzBlog.BlogPostIds.AddRange(blogPosts.Where(x => x.BlogId == mythzBlog.Id).ConvertAll(x => x.Id));
redisUsers.Store(ayende);
redisUsers.Store(mythz);
redisBlogs.StoreAll(new[] { ayendeBlog, mythzBlog });
redisBlogPosts.StoreAll(blogPosts);
}
}
可以看到,使用`IRedisClient`我们能够使用`StoreAll`等批量操作,这使得事情变得容易。
删除数据
private void DeleteSpecificBlog(long blogId)
{
Console.WriteLine("DELETING SINGLE Blog\r\n");
using (var redisBlogs = redisClient.GetTypedClient<Blog>())
{
redisBlogs.DeleteById(blogId);
redisBlogs.Save();
}
}
这里有一个**重要**的注意事项,`Save()`方法并非看起来那么简单。这实际上执行的是前台/同步的磁盘快照保存——您通常不希望在生产环境中这样做。对于生产环境,您最好执行 BGSAVE(后台保存)。
Linq 支持
正如我已经多次声明的那样,Redis 在内存数据集上工作,也就是说整个数据集必须适应内存,因此它公开了许多集合类来管理内存存储。在 Service Stack Redis 客户端中,这些集合通常使用标准 .NET 集合类进行管理,因此任何标准的 LINQ 扩展方法都可以直接应用于这些集合。例如,这里是我获取`List
private IList<Blog> Blogs()
{
IList<Blog> blogs = new List<Blog>();
using (var redisBlogs = redisClient.GetTypedClient<Blog>())
{
blogs = redisBlogs.GetAll().Where(x => x.Id > 20).ToList();
}
return blogs;
}
当然应该注意的是,由于 Redis 使用内存数据集,它不是使用真正的 IQueryProvider,而实际上只是使用 Linq to objects,数据库中没有发生延迟加载。所以请注意这个事实
事务支持
在 Service Stack Redis 客户端中,事务通过使用`CreateTransaction()`方法从`IRedisClient`获取新的`IRedisTransaction`来管理。获取`IRedisTransaction`后,您只需使用以下方法来处理`IRedisTransaction`
- `QueueCommand(..)`:将包含的命令列入事务中
- `Rollback()`:将回滚事务
- `Commit()`:将提交事务
总之,这里是一些演示代码
public void InsertInsideTransaction(bool shouldTransactionRollback)
{
RedisClient transClient = new RedisClient("localhost");
ClearAll();
using (var trans = transClient.CreateTransaction())
{
var redisUsers = redisClient.GetTypedClient<User>();
//Have to do this here (as redisUsers.GetNextSequence() is a READ, which MUST be done before
//we write using the RedisTransaction)
var sacha = new User { Id = redisUsers.GetNextSequence(), Name = "Sacha Barber" };
trans.QueueCommand(r =>
{
using (redisUsers = r.GetTypedClient<User>())
{
redisUsers.Store(sacha);
}
});
//commit or rollback based on incoming flag
if (shouldTransactionRollback)
trans.Rollback();
else
trans.Commit();
IList<User> users = Users();
Console.WriteLine(string.Format("InsertInsideTransaction : There are currently {0}, Users", users.Count()));
}
}
缓存过期
很抱歉一直强调这一点,但 Redis 如此之快的原因在于它完全在内存中处理数据集。那么,那些你不再需要的数据会发生什么呢?有没有办法删除它?嗯,我们可以通过编程方式删除,但有没有办法将 Redis 用作某种最近最常使用(MRU)缓存,让数据自行过期?
事实证明这是可能的,我们需要做的是使用 Redis Service Stack 类型化客户端的标准方法之一`ExpireIn`,它的方法签名如下所示
bool ExpireIn(object id, TimeSpan expiresAt);
为了演示这一点,我在演示中提供了一小段代码,如下所示
public void CacheInsertWhichDeletes()
{
ClearAll();
using (var redisUsers = redisClient.GetTypedClient<User>())
{
var frankenFurter = new User { Id = redisUsers.GetNextSequence(), Name = "FrankenFurter" };
redisUsers.Store(frankenFurter);
redisUsers.ExpireIn(frankenFurter.Id, TimeSpan.FromSeconds(1));
User fetchedUser = redisUsers.GetById(frankenFurter.Id);
Console.WriteLine(fetchedUser != null ? "Still exists" : "Removed from cache");
Thread.Sleep(2000);
fetchedUser = redisUsers.GetById(frankenFurter.Id);
Console.WriteLine(fetchedUser != null ? "Still exists" : "Removed from cache");
}
}
这就是你管理快速移动数据的方式,你只希望数据存在 x 时间。
池化
Service Stack(我选择使用的 Redis 客户端)通过使用 2 个类`PooledRedisClientManager`和任何实现 Servce Stack 接口`IRedisClientFactory`的自定义类来提供连接池。
以下代码声明了多个客户端地址
readonly string[] testReadWriteHosts = new[] {
"readwrite1", "readwrite2:6000", "192.168.0.1", "localhost"
};
readonly string[] testReadOnlyHosts = new[] {
"read1", "read2:7000", "127.0.0.1"
};
然后我们继续这样创建一个`PooledRedisClientManager`
private PooledRedisClientManager CreateAndStartManager()
{
var manager = CreateManager();
manager.Start();
return manager;
}
private PooledRedisClientManager CreateManager()
{
return CreateManager(new RedisClientFactory(), testReadWriteHosts, testReadOnlyHosts);
}
private PooledRedisClientManager CreateManager(
IRedisClientFactory clientFactory, string[] readWriteHosts, string[] readOnlyHosts)
{
return new PooledRedisClientManager(readWriteHosts, readOnlyHosts,
new RedisClientManagerConfig
{
MaxWritePoolSize = readWriteHosts.Length,
MaxReadPoolSize = readOnlyHosts.Length,
AutoStart = false,
})
{
RedisClientFactory = clientFactory
};
}
可以看到,我们将`RedisClientFactory`设置为`IRedisClientFactory`的一个实例。那么,它看起来像什么呢?它看起来像这样
public class RedisClientFactory : IRedisClientFactory
{
public RedisClient CreateRedisClient(string host, int port)
{
return new RedisClient(host, port);
}
}
完成这些步骤后,剩下的就是启动并使用一个新的`PooledRedisClientManager`。我们通过`PooledRedisClientManager`获得客户端访问权限。在演示代码中,我在新线程中启动新客户端以模拟对连接池的并发访问,以下是相关代码
/// <summary>
/// Use the PooledRedisClientManager to gain access to n-many clients
/// </summary>
public void Start()
{
Thread t = new Thread((state) =>
{
const int noOfConcurrentClients = 5; //WaitHandle.WaitAll limit is <= 64
var clientUsageMap = new Dictionary<string, int>();
var clientAsyncResults = new List<IAsyncResult>();
using (var manager = CreateAndStartManager())
{
for (var i = 0; i < noOfConcurrentClients; i++)
{
var clientNo = i;
var action = (Action)(() => UseClient(manager, clientNo, clientUsageMap));
clientAsyncResults.Add(action.BeginInvoke(null, null));
}
}
WaitHandle.WaitAll(clientAsyncResults.ConvertAll(x => x.AsyncWaitHandle).ToArray());
Console.WriteLine(TypeSerializer.SerializeToString(clientUsageMap));
var hostCount = 0;
foreach (var entry in clientUsageMap)
{
hostCount += entry.Value;
}
});
t.SetApartmentState(ApartmentState.MTA);
t.Start();
}
private static void UseClient(IRedisClientsManager manager, int clientNo,
Dictionary<string, int> hostCountMap)
{
using (IRedisClient client = manager.GetClient())
{
lock (hostCountMap)
{
int hostCount;
if (!hostCountMap.TryGetValue(client.Host, out hostCount))
{
hostCount = 0;
}
hostCountMap[client.Host] = ++hostCount;
}
Console.WriteLine("Client '{0}' is using '{1}'", clientNo, client.Host);
//YOU COULD USE THE SPECIFIC CLIENT HERE, YOU MAY HAVE TO TEST THE HOST TO SEE IF ITS THE ACTUAL ONE YOU WANT
//YOU COULD USE THE SPECIFIC CLIENT HERE, YOU MAY HAVE TO TEST THE HOST TO SEE IF ITS THE ACTUAL ONE YOU WANT
//YOU COULD USE THE SPECIFIC CLIENT HERE, YOU MAY HAVE TO TEST THE HOST TO SEE IF ITS THE ACTUAL ONE YOU WANT
//YOU COULD USE THE SPECIFIC CLIENT HERE, YOU MAY HAVE TO TEST THE HOST TO SEE IF ITS THE ACTUAL ONE YOU WANT
}
}
它的用法有点倒退,你必须向`PooledRedisClientManager`请求一个`IRedisClient`,它会使用你提供的`IRedisClientFactory`,然后给你一个`IRedisClient`供你使用。但是你得到哪个`IRedisClient`取决于`PooledRedisClientManager`。所以如果你依赖它是一个特定的`IRedisClient`,那你就猜错了,你需要检查`PooledRedisClientManager`分配了哪个`IRedisClient`。
Redis 管理界面
有一个管理 UI(运行在 .NET/Mono 上),它还包括所有 Redis 操作的 JSON、XML、JSV 和 SOAP 服务,地址是
https://github.com/ServiceStack/ServiceStack.RedisWebServices/
Raven 文档数据库用法
在本节中,我将讨论使用 Raven。
在我们开始使用 Raven 之前,有几点值得注意,所以我们现在就来快速讨论一下吧
- Raven 完全用 .NET 编写,是的,甚至服务器也是 .NET。我见过很多关于这个的聊天/网络噪音,人们说它对于高容量数据集需求来说不够快。坦白说,我无法确定情况是否如此,因为我在评估各种不同的文档数据库时只是处于评估模式。不过,我可以说的是,在我的评估中,我没有发现任何问题。
- Raven 有一个拒绝无界结果集的概念,所以如果你尝试取回太多数据,Raven 会介入并阻止。这是一个可以更改但并不鼓励更改的设置
- Raven 借鉴了其他著名框架的思想,主要是 NHibernate,所以当你看到`IDocumentSession`时,它应该看起来相当熟悉,并且几乎与 NHibernate 中的`ISession`用法相同。
- Raven 的商业版本不是免费的,但如果它符合你的需求,价格也不贵
服务器
Raven 服务器(可在此处下载:http://ravendb.net/download)是用 C# 编写的,可以使用“`Raven.Server.exe`”进程运行。事实上,当你下载 Raven 服务器后,你应该会看到类似这样的东西
其中有许多不同的进程可用于管理 Raven 服务器。
.NET 客户端
Raven 只有一个 .NET 客户端,即下载页面上提供的客户端:http://ravendb.net/download
我几乎不可能概述 Raven 的所有功能,但我将概述我认为在开始使用 Raven 时最重要的部分。
第一步
你必须运行 Raven 实际服务器。我已经尝试通过创建一个名为“`RavenServer`”的帮助类来简化此操作,你应该修改它以指向你的 Raven 服务器下载位置。一旦 Raven 实际服务器运行起来,我们需要创建一个软件`DocumentStore`(它连接到实际的 Raven 服务器实例)。
下面显示了一些骨架代码,所有 Raven 代码在附加的演示代码中都使用了这些代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Raven.Client.Document;
using Raven.Client;
using System.IO;
namespace DocumentDB.Raven
{
class Program
{
public static string documentStoreLocation = @"https://:8080";
[STAThread]
static void Main(string[] args)
{
Run();
}
public static void Run()
{
try
{
if (RavenServer.Instance.Start())
{
using (var documentStore = new DocumentStore { Url = documentStoreLocation, DefaultDatabase = "ravenTest" })
{
//Use DocumentStore here
//Use DocumentStore here
//Use DocumentStore here
//Use DocumentStore here
//Use DocumentStore here
//Use DocumentStore here
}
}
}
catch (Exception ex)
{
Console.WriteLine("============= OH NO : ERROR ============");
}
Console.ReadLine();
}
}
}
这个软件`DocumentStore`随后被演示代码中的各种类使用,因此你可以在附加的演示代码中看到`DocumentStore`对象的使用。
使用类型化对象的基本 CRUD 操作
为了向您展示如何使用我选择的 Raven 客户端,您真正需要知道的只是如何使用`IDocumentSession`的实例,它通常按以下方式使用,其中查询使用您想要获取数据的文档的通用类型运行
private IList<Blog> Blogs() { IList<Blog> blogs = new List<Blog>(); using (IDocumentSession session = documentStore.OpenSession()) { blogs = session.Query<Blog>().ToList(); } return blogs; }
下面列出了各种操作的示例,展示了如何使用 Service Stack `RedisClient`执行各种操作,其中使用了以下文档类来存储
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace DocumentDB.Raven
{
public class Blog
{
public Blog()
{
this.Tags = new List<string>();
this.BlogPostIds = new List<long>();
}
public long Id { get; set; }
public long UserId { get; set; }
public string UserName { get; set; }
public List<string> Tags { get; set; }
public List<long> BlogPostIds { get; set; }
}
public class BlogPost
{
public BlogPost()
{
this.Categories = new List<string>();
this.Tags = new List<string>();
}
public long Id { get; set; }
public long BlogId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public List<string> Categories { get; set; }a
public List<string> Tags { get; set; }
}
public class User
{
public User()
{
this.BlogIds = new List<long>();
}
public long Id { get; set; }
public string Name { get; set; }
public List<long> BlogIds { get; set; }
}
}
获取数据
private IList<Blog> Blogs()
{
IList<Blog> blogs = new List<Blog>();
using (IDocumentSession session = documentStore.OpenSession())
{
blogs = session.Query<Blog>().ToList();
}
return blogs;
}
private IList<User> Users()
{
IList<User> users = new List<User>();
using (IDocumentSession session = documentStore.OpenSession())
{
users = session.Query<User>().ToList();
}
return users;
}
插入数据
public void InsertTestData()
{
using (IDocumentSession session = documentStore.OpenSession())
{
var ayende = new User { Name = "Oren Eini" };
var mythz = new User { Name = "Demis Bellot" };
var ayendeBlog = new Blog
{
UserId = ayende.Id,
UserName = ayende.Name,
Tags = new List<string> { "Architecture", ".NET", "Databases" },
};
var mythzBlog = new Blog
{
UserId = mythz.Id,
UserName = mythz.Name,
Tags = new List<string> { "Architecture", ".NET", "Databases" },
};
session.Store(ayende);
session.Store(mythz);
session.Store(ayendeBlog);
session.Store(mythzBlog);
session.SaveChanges();
var mythzBlogPosts = new List<BlogPost>
{
new BlogPost
{
BlogId = mythzBlog.Id,
Title = "Redis",
Categories = new List<string> { "NoSQL", "Cache" },
Tags = new List<string> {"Redis", "NoSQL", "Scalability", "Performance"},
},
new BlogPost
{
BlogId = mythzBlog.Id,
Title = "Couch Db",
Categories = new List<string> { "NoSQL", "DocumentDB" },
Tags = new List<string> {"CouchDb", "NoSQL", "JSON"},
}
};
var ayendeBlogPosts = new List<BlogPost>
{
new BlogPost
{
BlogId = ayendeBlog.Id,
Title = "RavenDB",
Categories = new List<string> { "NoSQL", "DocumentDB" },
Tags = new List<string> {"Raven", "NoSQL", "JSON", ".NET"} ,
},
new BlogPost
{
BlogId = ayendeBlog.Id,
Title = "Cassandra",
Categories = new List<string> { "NoSQL", "Cluster" },
Tags = new List<string> {"Cassandra", "NoSQL", "Scalability", "Hashing"},
}
};
foreach (BlogPost blogPost in ayendeBlogPosts.Union(mythzBlogPosts))
{
session.Store(blogPost);
}
session.SaveChanges();
ayende.BlogIds.Add(ayendeBlog.Id);
ayendeBlog.BlogPostIds.AddRange(ayendeBlogPosts.Select(x => x.Id));
mythz.BlogIds.Add(mythzBlog.Id);
mythzBlog.BlogPostIds.AddRange(mythzBlogPosts.Select(x => x.Id));
session.Store(ayende);
session.Store(mythz);
session.Store(ayendeBlog);
session.Store(mythzBlog);
session.SaveChanges();
}
}
删除数据
private void DeleteSpecificBlog(long blogId)
{
Console.WriteLine("DELETING SINGLE Blog\r\n");
using (IDocumentSession session = documentStore.OpenSession())
{
session.Delete<Blog>(session.Query<Blog>().Where(x => x.Id == blogId).Single());
session.SaveChanges();
}
}
LINQ 支持
由于 Raven 完全用 .NET 构建,如果它不支持 LINQ 会很奇怪,而且事实确实如此。例如,这里是我获取`List
private IList<Blog> Blogs()
{
IList<Blog> blogs = new List<Blog>();
using (IDocumentSession session = documentStore.OpenSession())
{
blogs = session.Query<Blog>().ToList();
}
return blogs;
}
事务支持
Raven 全力支持事务,由于 Raven 完全用 .NET 编写,你甚至可以使用熟悉的事务类,例如`TransactionScope`,我不得不说这确实让生活更轻松。这里是使用 Raven 事务的示例。
public void InsertInsideTransaction(bool shouldTransactionRollback)
{
var users = new List<User>();
using (IDocumentSession session = documentStore.OpenSession())
{
try
{
users = session.Query<User>().ToList();
Console.WriteLine(string.Format("Before Transaction : There are currently {0}, Users", users.Count()));
using (var transaction = new TransactionScope())
{
var sacha = new User { Name = "Sacha Barber" };
session.Store(sacha);
session.SaveChanges();
if (shouldTransactionRollback)
throw new InvalidOperationException("testing transactions");
transaction.Complete();
}
users = session.Query<User>().ToList();
Console.WriteLine(string.Format("After Transaction : There are currently {0}, Users", users.Count()));
}
catch
{
users = session.Query<User>().ToList();
Console.WriteLine(string.Format("On Transaction Error : There are currently {0}, Users", users.Count()));
}
}
}
直接数据库操作
有时您只需要直接访问底层的 Raven 数据库命令。在 Raven 中,这通过使用`DocumentStore.DatabaseCommands`属性来完成,该属性将为您提供一个`IDatabaseCommands`实例,允许您执行各种任务。下面显示了直接来自 Raven 的`IDatabaseCommands`接口定义,它向您展示了您可以使用`IDatabaseCommands`实例执行哪些操作
using Raven.Abstractions.Commands;
using Raven.Abstractions.Data;
using Raven.Abstractions.Indexing;
using Raven.Client.Connection.Profiling;
using Raven.Client.Indexes;
using Raven.Json.Linq;
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.IO;
using System.Net;
namespace Raven.Client.Connection
{
// Summary:
// Expose the set of operations by the RavenDB server
public interface IDatabaseCommands : IHoldProfilingInformation
{
// Summary:
// Gets or sets the operations headers
NameValueCollection OperationsHeaders { get; set; }
//
// Summary:
// Gets a value indicating whether [supports promotable transactions].
bool SupportsPromotableTransactions { get; }
// Summary:
// Executed the specified commands as a single batch
//
// Parameters:
// commandDatas:
// The command data.
BatchResult[] Batch(IEnumerable<ICommandData> commandDatas);
//
// Summary:
// Commits the specified tx id
//
// Parameters:
// txId:
// The tx id.
void Commit(Guid txId);
//
// Summary:
// Deletes the document with the specified key
//
// Parameters:
// key:
// The key.
//
// etag:
// The etag.
void Delete(string key, Guid? etag);
//
// Summary:
// Deletes the attachment with the specified key
//
// Parameters:
// key:
// The key.
//
// etag:
// The etag.
void DeleteAttachment(string key, Guid? etag);
//
// Summary:
// Perform a set based deletes using the specified index, not allowing the operation
// if the index is stale
//
// Parameters:
// indexName:
// Name of the index.
//
// queryToDelete:
// The query to delete.
void DeleteByIndex(string indexName, IndexQuery queryToDelete);
//
// Summary:
// Perform a set based deletes using the specified index
//
// Parameters:
// indexName:
// Name of the index.
//
// queryToDelete:
// The query to delete.
//
// allowStale:
// if set to true [allow stale].
void DeleteByIndex(string indexName, IndexQuery queryToDelete, bool allowStale);
//
// Summary:
// Deletes the specified index
//
// Parameters:
// name:
// The name.
void DeleteIndex(string name);
//
// Summary:
// Disable all caching within the given scope
IDisposable DisableAllCaching();
//
// Summary:
// Create a new instance of Raven.Client.Connection.IDatabaseCommands that will
// interacts with the specified database
IDatabaseCommands ForDatabase(string database);
//
// Summary:
// Create a new instance of Raven.Client.Connection.IDatabaseCommands that will
// interacts with the default database
IDatabaseCommands ForDefaultDatabase();
//
// Summary:
// Retrieves the document for the specified key
//
// Parameters:
// key:
// The key.
JsonDocument Get(string key);
//
// Summary:
// Retrieves documents with the specified ids, optionally specifying includes
// to fetch along
//
// Parameters:
// ids:
// The ids.
//
// includes:
// The includes.
MultiLoadResult Get(string[] ids, string[] includes);
//
// Summary:
// Retrieves the attachment with the specified key
//
// Parameters:
// key:
// The key.
Attachment GetAttachment(string key);
//
// Summary:
// Returns the names of all tenant databases on the RavenDB server
//
// Returns:
// List of tenant database names
string[] GetDatabaseNames();
//
// Summary:
// Using the given Index, calculate the facets as per the specified doc
IDictionary<string, IEnumerable<FacetValue>> GetFacets(string index, IndexQuery query, string facetSetupDoc);
//
// Summary:
// Gets the index definition for the specified name
//
// Parameters:
// name:
// The name.
IndexDefinition GetIndex(string name);
//
// Summary:
// Returns the names of all indexes that exist on the server
//
// Parameters:
// start:
// Paging start
//
// pageSize:
// Size of the page.
string[] GetIndexNames(int start, int pageSize);
//
// Summary:
// Create a new instance of Raven.Client.Connection.IDatabaseCommands that will
// interact with the root database. Useful if the database has works against
// a tenant database
IDatabaseCommands GetRootDatabase();
//
// Summary:
// Retrieve the statistics for the database
DatabaseStatistics GetStatistics();
//
// Summary:
// Get the all terms stored in the index for the specified field You can page
// through the results by use fromValue parameter as the starting point for
// the next query
IEnumerable<string> GetTerms(string index, string field, string fromValue, int pageSize);
//
// Summary:
// Retrieves the document metadata for the specified document key.
//
// Parameters:
// key:
// The key.
//
// Returns:
// The document metadata for the specifed document, or null if the document
// does not exist
JsonDocumentMetadata Head(string key);
//
// Summary:
// Perform a single POST requst containing multiple nested GET requests
GetResponse[] MultiGet(GetRequest[] requests);
//
// Summary:
// Sends a patch request for a specific document, ignoring the document's Etag
//
// Parameters:
// key:
// Id of the document to patch
//
// patches:
// Array of patch requests
void Patch(string key, PatchRequest[] patches);
//
// Summary:
// Sends a patch request for a specific document
//
// Parameters:
// key:
// Id of the document to patch
//
// patches:
// Array of patch requests
//
// etag:
// Require specific Etag [null to ignore]
void Patch(string key, PatchRequest[] patches, Guid? etag);
//
// Summary:
// Promotes the transaction
//
// Parameters:
// fromTxId:
// From tx id.
byte[] PromoteTransaction(Guid fromTxId);
//
// Summary:
// Puts the document in the database with the specified key
//
// Parameters:
// key:
// The key.
//
// etag:
// The etag.
//
// document:
// The document.
//
// metadata:
// The metadata.
PutResult Put(string key, Guid? etag, RavenJObject document, RavenJObject metadata);
//
// Summary:
// Puts a byte array as attachment with the specified key
//
// Parameters:
// key:
// The key.
//
// etag:
// The etag.
//
// data:
// The data.
//
// metadata:
// The metadata.
void PutAttachment(string key, Guid? etag, Stream data, RavenJObject metadata);
//
// Summary:
// Creates an index with the specified name, based on an index definition
//
// Parameters:
// name:
// The name.
//
// indexDef:
// The index def.
string PutIndex(string name, IndexDefinition indexDef);
//
// Summary:
// Creates an index with the specified name, based on an index definition that
// is created by the supplied IndexDefinitionBuilder
//
// Parameters:
// name:
// The name.
//
// indexDef:
// The index def.
//
// Type parameters:
// TDocument:
// The type of the document.
//
// TReduceResult:
// The type of the reduce result.
string PutIndex<TDocument, TReduceResult>(string name, IndexDefinitionBuilder<TDocument, TReduceResult> indexDef);
//
// Summary:
// Creates an index with the specified name, based on an index definition
//
// Parameters:
// name:
// The name.
//
// indexDef:
// The index def.
//
// overwrite:
// if set to true [overwrite].
string PutIndex(string name, IndexDefinition indexDef, bool overwrite);
//
// Summary:
// Creates an index with the specified name, based on an index definition that
// is created by the supplied IndexDefinitionBuilder
//
// Parameters:
// name:
// The name.
//
// indexDef:
// The index def.
//
// overwrite:
// if set to true [overwrite].
//
// Type parameters:
// TDocument:
// The type of the document.
//
// TReduceResult:
// The type of the reduce result.
string PutIndex<TDocument, TReduceResult>(string name, IndexDefinitionBuilder<TDocument, TReduceResult> indexDef, bool overwrite);
//
// Summary:
// Queries the specified index in the Raven flavoured Lucene query syntax
//
// Parameters:
// index:
// The index.
//
// query:
// The query.
//
// includes:
// The includes.
QueryResult Query(string index, IndexQuery query, string[] includes);
//
// Summary:
// Resets the specified index
//
// Parameters:
// name:
// The name.
void ResetIndex(string name);
//
// Summary:
// Rollbacks the specified tx id
//
// Parameters:
// txId:
// The tx id.
void Rollback(Guid txId);
//
// Summary:
// Retrieves documents for the specified key prefix
JsonDocument[] StartsWith(string keyPrefix, int start, int pageSize);
//
// Summary:
// Stores the recovery information
//
// Parameters:
// resourceManagerId:
// The resource manager Id for this transaction
//
// txId:
// The tx id.
//
// recoveryInformation:
// The recovery information.
void StoreRecoveryInformation(Guid resourceManagerId, Guid txId, byte[] recoveryInformation);
//
// Summary:
// Returns a list of suggestions based on the specified suggestion query
//
// Parameters:
// index:
// The index to query for suggestions
//
// suggestionQuery:
// The suggestion query.
SuggestionQueryResult Suggest(string index, SuggestionQuery suggestionQuery);
//
// Summary:
// Perform a set based update using the specified index, not allowing the operation
// if the index is stale
//
// Parameters:
// indexName:
// Name of the index.
//
// queryToUpdate:
// The query to update.
//
// patchRequests:
// The patch requests.
void UpdateByIndex(string indexName, IndexQuery queryToUpdate, PatchRequest[] patchRequests);
//
// Summary:
// Perform a set based update using the specified index
//
// Parameters:
// indexName:
// Name of the index.
//
// queryToUpdate:
// The query to update.
//
// patchRequests:
// The patch requests.
//
// allowStale:
// if set to true [allow stale].
void UpdateByIndex(string indexName, IndexQuery queryToUpdate, PatchRequest[] patchRequests, bool allowStale);
//
// Summary:
// Returns a new Raven.Client.Connection.IDatabaseCommands using the specified
// credentials
//
// Parameters:
// credentialsForSession:
// The credentials for session.
IDatabaseCommands With(ICredentials credentialsForSession);
}
}
这里有一个使用它添加索引的小例子
this.documentStore.DatabaseCommands.PutIndex("Users/ByName",
new IndexDefinitionBuilder<User>
{
Map = users => from user in users
select new { user.Name }
});
全文搜索
Raven 允许您做的更有趣的事情之一是进行全文搜索。为此,我们通常会创建一个索引,并在其中提供 Map LINQ 表达式来构建索引。这是一个例子
public class User_ByName_FullTextSearch : AbstractIndexCreationTask<User>
{
public User_ByName_FullTextSearch()
{
Map = users => from user in users
select new { user.Name };
Index(x => x.Name, FieldIndexing.Analyzed);
}
public override string IndexName
{
get
{
return @"Users\ByNameIndex";
}
}
}
然后我们可以简单或复杂地使用此`Index`,如下所示
简单案例
Console.WriteLine(string.Format("Looking for users with Name starting with '{0}'\r\n", searchName));
Console.WriteLine("Simple starts with example:");
foreach (var person in Queryable.Where(session.Query<User, User_ByName_FullTextSearch>(), x => x.Name.StartsWith(searchName)))
{
Console.WriteLine(person.Name);
}
复杂案例
Console.WriteLine(string.Format("Looking for users with Name starting with '{0}'\r\n", searchName));
Console.WriteLine("Simple starts with example:");
IQueryable<User> query = session.Query<User, User_ByName_FullTextSearch>();
query = searchName.Split().Aggregate(query, (current, part) => current.Where(x => x.Name.StartsWith(part)));
foreach (var person in query)
{
Console.WriteLine(person.Name);
}
我们将看到以下结果
批量操作
本节将概述如何使用 Raven 执行批量操作。
批量更新
有时您可能需要更新/删除一整批数据。Raven 通过使用所谓的“基于集合的操作”来支持这类操作。要在 Raven 中执行“基于集合的操作”,您必须使用它的“修补 API”。我们现在将看一些如何使用 Raven 的“修补 API”的示例。
Raven 的修补 API 核心是您需要熟悉的两个对象,它们是
- `PatchCommandData`:单个批处理操作,用于文档修补
- `PatchRequest`:针对指定文档的修补请求
这是一个批量更新的例子
public void DoUpdate(int numUsers)
{
this.documentStore.DatabaseCommands.Batch(
//patch 1st 1/2 of users
Enumerable.Range(0, numUsers / 2).Select(i => new PatchCommandData
{
Key = "users/" + i,
Patches = new[]
{
new PatchRequest
{
Name = "Name",
Value = "Users-" + i
},
}
}
).ToArray());
this.documentStore.DatabaseCommands.Batch(
//patch 1st 1/2 of users
Enumerable.Range(numUsers / 2, numUsers).Select(i => new PatchCommandData
{
Key = "users/" + i,
Patches = new[]
{
new PatchRequest
{
Name = "Name",
Value = "Users-" + i
},
}
}
).ToArray());
using (IDocumentSession session = documentStore.OpenSession())
{
var users = session.Query<User>()
.Customize(x => x.WaitForNonStaleResults(TimeSpan.FromSeconds(10)))
.ToList();
for (int i = 0; i < numUsers; i++)
{
Console.WriteLine("Expecting UserName of '{0}', actual UserName now is '{1}'", "Users-"
+ (i + 1), users[i].Name);
}
}
}
此代码依赖于存在一个指定索引(Raven 使用 LINQ 查询构建)的索引,以放置一个“UsersByName”索引,`PatchRequest`在执行批量更新以获取正确文档时可以使用该索引。
this.documentStore.DatabaseCommands.PutIndex("Users/ByName",
new IndexDefinitionBuilder<User>
{
Map = users => from user in users
select new { user.Name }
});
运行此代码会产生类似这样的输出
批量删除
要执行批量删除,我们只需要一个索引,就像我们之前用于批量更新的索引一样。
this.documentStore.DatabaseCommands.PutIndex("Users/ByName",
new IndexDefinitionBuilder<User>
{
Map = users => from user in users
select new { user.Name }
});
然后我们可以简单地向`DeleteByIndex`发出`DatabaseCommand`,其中我们指定我们的`Index`字符串,并且我们还提供一个`IndexQuery`来选择我们想要匹配的文档,这些文档将被删除。
this.documentStore.DatabaseCommands.DeleteByIndex("Users/ByName",
new IndexQuery
{
Query = "Name:*" // where entity.Name contains anything
}, allowStale: false);
工作室
Raven 附带了一个名为“The Studio”的 Sliverlight Web 客户端应用程序,它是一个图形化前端工具,可用于执行各种任务,例如
- 管理集合/索引/文档
- 编辑文档
- 创建文档
- 导入数据库(使用 Smuggler.exe 工具)
- 导出数据库(使用 Smuggler.exe 工具)
这是一张正在运行的 Raven Studio 实例的截图,它反映了一些演示代码(点击放大图片)
MongoDB 文档数据库用法
在本节中,我将讨论使用 MongoDB
服务器
MongoDB 服务器(可在此处下载:https://mongodb.ac.cn/downloads)是用 C++ 编写的,可以使用“`mongod.exe`”进程运行。事实上,当你下载 MongoDB 后,你应该会看到类似这样的东西
其中有许多不同的进程可用于管理 MongoDB。
.NET 客户端
本文使用的 .NET 客户端是官方(受支持的).NET 客户端,可从 https://github.com/mongodb/mongo-csharp-driver/downloads 下载。我几乎不可能概述 MongoDB 的所有功能,但我将概述我认为在开始使用 MongoDB 时最重要的部分。
第一步
你必须运行 MongoDB 实际服务器。我已经尝试通过创建一个名为“`MongoDBServer`”的帮助类来简化此操作,你应该修改它以指向你的 MongoDB 下载位置。一旦 MongoDB 实际服务器运行起来,我们需要创建一个软件`MongoServer`(它连接到实际的 MongoDB 服务器实例)。
下面显示了一些骨架代码,所有 MongoDB 代码在附加的演示代码中都使用了这些代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using MongoDB.Bson;
using MongoDB.Driver;
using MongoDB.Driver.Linq;
using MongoDB.Bson.Serialization.Attributes;
namespace DocumentDB.Mongo
{
class Program
{
public static void Run()
{
try
{
if (MongoDBServer.Instance.Start())
{
MongoServer server = MongoServer.Create(); // connect to localhost
//Use the software server
//Use the software server
//Use the software server
//Use the software server
}
}
catch (Exception ex)
{
Console.WriteLine("============= OH NO : ERROR ============");
}
Console.ReadLine();
}
static void Main(string[] args)
{
Run();
}
}
}
这个软件`MongoServer`随后被演示代码中的各种类使用,因此你可以在附加的演示代码中看到`MongoServer`对象的使用。
原始文档存储
现在我们已经看到需要启动实际的 MongoDB 服务器以及一个软件`MongoServer`实例,让我们看看如何存储一些文档。
这里有一些代码,可以存储 MongoDB 使用的`BsonDocument`。
MongoDatabase rawDocumentDemoDB = server.GetDatabase("rawDocumentDemoDB");
rawDocumentDemoDB.Drop();
MongoCollection<BsonDocument> users = rawDocumentDemoDB.GetCollection("users");
// Create BsonDocument object for new user
var user = new BsonDocument();
user["firstname"] = "Goat";
user["lastname"] = "Head";
user["age"] = 12;
user["createdate"] = DateTime.Now;
// Insert new user object to collection
users.Insert(user);
这里有一些代码可以更新`BsonDocument`
MongoDatabase rawDocumentDemoDB = server.GetDatabase("rawDocumentDemoDB");
rawDocumentDemoDB.Drop();
MongoCollection<BsonDocument> users = rawDocumentDemoDB.GetCollection("users");
var savedUsers = users.FindAll();
if (savedUsers.Any())
{
BsonDocument userToModify = savedUsers.First();
if (userToModify != null)
{
Console.WriteLine(string.Format("Decrementing User Id : {0}, Age By -1", userToModify["_id"]));
userToModify["age"] = userToModify["age"].AsInt32 - 1;
users.Save(userToModify);
}
}
可以看出,在使用 MongoDB 时,我们与`MongoCollection
是的,结果证明有,让我们看看。
序列化你自己的类型
好的,我们已经看到了如何处理`BsonDocument`对象并发现它们存在不足,现在我们想序列化我们自己的对象,那么我们该怎么做呢?这很简单,只需执行以下操作
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Bson;
namespace DocumentDB.Mongo
{
public class Blog
{
public Blog()
{
this.Tags = new List<string>();
this.BlogPostIds = new List<ObjectId>();
}
public ObjectId Id { get; set; }
public ObjectId UserId { get; set; }
public string UserName { get; set; }
public List<string> Tags { get; set; }
public List<ObjectId> BlogPostIds { get; set; }
}
}
我们还可以通过使用特殊的 Mongo 属性来自定义序列化过程,如下所示,我已为相同的 Blog 类添加了属性:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Bson;
namespace DocumentDB.Mongo
{
public class Blog
{
public Blog()
{
this.Tags = new List<string>();
this.BlogPostIds = new List<ObjectId>();
}
[BsonElementAttribute("id")]
public ObjectId Id { get; set; }
[BsonElementAttribute("userid")]
public ObjectId UserId { get; set; }
[BsonElementAttribute("username")]
public string UserName { get; set; }
[BsonElementAttribute("tags")]
public List<string> Tags { get; set; }
[BsonElementAttribute("blogpostids")]
public List<ObjectId> BlogPostIds { get; set; }
}
}
这里有几点:
- 我们使用`BsonElementAttribute`来标记我们的成员是可序列化的
- ID 使用`ObjectId`,这是一种 MongoDB 类型。您可以使用 long/int,但如果您使用`ObjectId`,MongoDB 会自动为您填充此 ID。
-
我将我的模型设计为使用链接到 ID 作为外键,这有两个原因
- 我不想在获取整个文档时带回一个巨大的图,我希望自己决定何时获取这些链接的数据。所以我只存储我可能感兴趣的其他数据的 ID,如果我发现需要,我稍后再查找
- BSON/JSON 不支持循环引用,所以存储 ID 并且没有循环会更简单
这种安排将允许这些类型的对象序列化为 BSON (Binary JSON)
我不得不说,我不太喜欢这种方式,因为它将持久化关注点放在我的模型中。我想 WCF 也通过其 DataContract/DataMember 属性这样做,所以差别不大,但对我来说,WCF 对象更像是 DTO。但嘿,你可以决定你喜欢与否。
使用类型化对象的基本 CRUD 操作
我认为展示使用强类型模型进行基本 CRUD 操作的最佳方式是直接展示完整的演示代码。所以,开始吧
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using MongoDB.Driver;
using MongoDB.Driver.Linq;
using MongoDB.Bson;
using MongoDB.Driver.Builders;
namespace DocumentDB.Mongo
{
public class MongoDemo
{
private MongoServer server;
private MongoDatabase mongoDemoDB;
public MongoDemo(MongoServer server)
{
this.server = server;
mongoDemoDB = server.GetDatabase("rawDocumentDemoDB");
mongoDemoDB.Drop();
}
public void ClearAll()
{
mongoDemoDB.Drop();
}
public void InsertFollowedByDelete()
{
StartRequest();
ClearAll();
InsertSingleBlog();
IList<Blog> blogs = Blogs();
Console.WriteLine(string.Format("There are currently {0}, Blogs", blogs.Count()));
DeleteSpecificBlog(blogs.First().Id);
blogs = Blogs();
Console.WriteLine(string.Format("There are currently {0}, Blogs", blogs.Count()));
StopRequest();
}
public void ShowListOfBlogs()
{
StartRequest();
Console.WriteLine("RECENT BLOGS:\r\n");
foreach (Blog blog in Blogs())
{
Console.WriteLine(blog.ToString());
}
StopRequest();
}
public int UserCount()
{
int users = 0;
StartRequest();
users = Users().Count();
StopRequest();
return users;
}
public void ShowListOfRecentPosts()
{
StartRequest();
Console.WriteLine("RECENT BLOG POSTS:\r\n");
foreach (BlogPost blogPost in BlogPosts())
{
Console.WriteLine(blogPost.ToString());
}
StopRequest();
}
public void InsertTestData()
{
StartRequest();
ClearAll();
var blogs = mongoDemoDB.GetCollection<Blog>("blogs");
var users = mongoDemoDB.GetCollection<User>("users");
var blogposts = mongoDemoDB.GetCollection<User>("blogposts");
List<User> newUsers = new List<User>();
User orenUser = new User { Name = "Oren Eini" };
User demisUser = new User { Name = "Demis Bellot" };
newUsers.Add(orenUser);
newUsers.Add(demisUser);
users.InsertBatch(newUsers);
List<Blog> newBlogs = new List<Blog>();
Blog orenBlog = new Blog
{
UserId = orenUser.Id,
UserName = orenUser.Name,
Tags = new List<string> { "Architecture", ".NET", "Databases" },
};
Blog demisBlog = new Blog
{
UserId = demisUser.Id,
UserName = demisUser.Name,
Tags = new List<string> { "Architecture", ".NET", "Databases" },
};
newBlogs.Add(orenBlog);
newBlogs.Add(demisBlog);
blogs.InsertBatch(newBlogs);
List<BlogPost> newBlogPosts = new List<BlogPost>
{
new BlogPost
{
BlogId = newBlogs.First().Id,
Title = "RavenDB",
Categories = new List<string> { "NoSQL", "DocumentDB" },
Tags = new List<string> {"Raven", "NoSQL", "JSON", ".NET"} ,
},
new BlogPost
{
BlogId = newBlogs.First().Id,
Title = "Redis",
Categories = new List<string> { "NoSQL", "Cache" },
Tags = new List<string> {"Redis", "NoSQL", "Scalability", "Performance"},
},
new BlogPost
{
BlogId = newBlogs.First().Id,
Title = "Cassandra",
Categories = new List<string> { "NoSQL", "Cluster" },
Tags = new List<string> {"Cassandra", "NoSQL", "Scalability", "Hashing"},
},
new BlogPost
{
BlogId = newBlogs.First().Id,
Title = "Couch Db",
Categories = new List<string> { "NoSQL", "DocumentDB" },
Tags = new List<string> {"CouchDb", "NoSQL", "JSON"},
}
};
blogposts.InsertBatch(newBlogPosts);
orenUser.BlogIds.Add(orenBlog.Id);
demisUser.BlogIds.Add(demisBlog.Id);
users.Save(orenUser);
users.Save(demisUser);
orenBlog.BlogPostIds.Add(newBlogPosts.First().Id);
demisBlog.BlogPostIds.Add(newBlogPosts.Last().Id);
blogs.Save(orenBlog);
blogs.Save(demisBlog);
foreach (BlogPost newBlogPost in newBlogPosts)
{
newBlogPost.BlogId = orenBlog.Id;
blogposts.Save(newBlogPost);
}
}
private void InsertSingleBlog()
{
var blogs = mongoDemoDB.GetCollection<Blog>("blogs");
var users = mongoDemoDB.GetCollection<User>("users");
var blogposts = mongoDemoDB.GetCollection<User>("blogposts");
User jayUser = new User { Name = "Jay Rays" };
users.Insert(jayUser);
Blog jayBlog = new Blog
{
UserId = jayUser.Id,
UserName = jayUser.Name,
Tags = new List<string> { "Architecture", ".NET", "Databases" },
};
blogs.Insert(jayBlog);
BlogPost jayBlogPost = new BlogPost
{
Title = "RavenDB",
Categories = new List<string> { "NoSQL", "DocumentDB" },
Tags = new List<string> { "Raven", "NoSQL", "JSON", ".NET" }
};
blogposts.Insert(jayBlogPost);
jayUser.BlogIds.Add(jayBlog.Id);
jayBlog.BlogPostIds.Add(jayBlogPost.Id);
jayBlogPost.BlogId = jayBlog.Id;
users.Save(jayUser);
blogs.Save(jayBlog);
blogposts.Save(jayBlogPost);
}
private IList<Blog> Blogs()
{
return mongoDemoDB.GetCollection<Blog>("blogs").AsQueryable<Blog>().Where(x => true).ToList();
}
private IList<User> Users()
{
return mongoDemoDB.GetCollection<User>("users").AsQueryable<User>().Where(x => true).ToList();
}
private IList<BlogPost> BlogPosts()
{
return mongoDemoDB.GetCollection<BlogPost>("blogposts").AsQueryable<BlogPost>().Where(x => true).ToList();
}
private void DeleteSpecificBlog(ObjectId blogId)
{
Console.WriteLine("DELETING SINGLE Blog\r\n");
mongoDemoDB.GetCollection<Blog>("blogs").Remove(Query.EQ("_id", blogId));
}
private void StartRequest()
{
server.RequestStart(mongoDemoDB);
}
private void StopRequest()
{
server.RequestDone();
}
}
}
希望这很容易理解,我觉得这很明显。
类 SQL 语法表达式
MongoDB 用户似乎喜欢的一点是,它通过使用 Query 类提供了类似 SQL 的 API。这是一个非常小的示例,用于删除具有特定 ID 的`Blog`
private void DeleteSpecificBlog(ObjectId blogId)
{
Console.WriteLine("DELETING SINGLE Blog\r\n");
mongoDemoDB.GetCollection<Blog>("blogs").Remove(Query.EQ("_id", blogId));
}
以下是一些您可能会使用的查询方法的示例
Linq 支持
MongoDB 确实支持 LINQ,您只需使用其集合并使用 AsQueryable 方法,然后您还可以执行典型的 LINQ 式操作,例如:
return mongoDemoDB.GetCollection<Blog>("blogs").AsQueryable<Blog>().ToList();
连接共享
MongoDB 的一个巧妙功能是它允许您共享到数据库的连接,以便一组操作都可以使用相同的连接。
这通过以下两种方法轻松实现
MongoDatabase rawDocumentDemoDB = server.GetDatabase("rawDocumentDemoDB");
server.RequestStart(rawDocumentDemoDB);
//DO STUFF HERE
//DO STUFF HERE
//DO STUFF HERE
//DO STUFF HERE
server.RequestDone();
批量操作
MongoDB 允许通过其集合的 xxxBatch() 方法进行批量操作,例如`InsertBatch(..)`,工作示例如下所示
MongoDatabase rawDocumentDemoDB = server.GetDatabase("rawDocumentDemoDB");
rawDocumentDemoDB.Drop();
MongoCollection<BsonDocument> users = rawDocumentDemoDB.GetCollection("users");
List<User> newUsers = new List<User>();
User orenUser = new User { Name = "Oren Eini" };
User demisUser = new User { Name = "Demis Bellot" };
newUsers.Add(orenUser);
newUsers.Add(demisUser);
users.InsertBatch(newUsers);
事务支持
这不是 MongoDB 提供的东西。
比较
在评估我选择的三种文档数据库时,这些是我从中得到的要点,正如我所说,这些是我自己的观点,只有我自己才能为此负责。我也更多地从初学者的角度出发,下表中的链接可能会对您有所帮助,如果您正在寻找更深入的答案,例如支持哪种复制机制,但从纯粹的零到简单 CRUD 可用性的角度来看,这就是我的想法。
Redis | Raven | Mongo |
|
|
|
我还发现这些对于进行有根据的比较非常有益
- http://kkovacs.eu/cassandra-vs-mongodb-vs-couchdb-vs-redis
- http://stackoverflow.com/questions/4720508/redis-couchdb-or-cassandra
- http://www.servicestack.net/mythz_blog/?p=474
至于我会选择使用哪种文档数据库,这实际上取决于我正在尝试做什么。例如,如果我有非常快速变化的数据,例如快速过期的外汇汇率或推文,我就会使用 Redis(我还听说 Cassandra 在这种情况下表现非常好,但我没有研究那种特定的文档数据库)。
如果我处理的是更标准的存储需求,这正是我们开始研究文档数据库的原因,那就是可伸缩性,我将使用 RavenDB,因为我发现它比 Mongo 更丰富、考虑得更周到。Mongo 似乎是很久以前编写的,并且正在显现出需要新意的迹象,事实上我读到过 Mongo 背后的团队正在开始一个新项目,我想那会很棒。然而,在撰写本文时,对于这种场景(即不需要非常快速移动的数据)我所研究的文档数据库中,我会选择 RavenDB。
就这些
这就是我想在本文中说的所有内容,希望您有所收获,并喜欢我对这个主题的滔滔不绝,并能看到有时基于 NoSQL 的解决方案可能正是您所需要的。