DocumentDB - 数据建模


虽然无模式数据库(例如 DocumentDB)可以非常轻松地接受数据模型的更改,但您仍然应该花一些时间思考您的数据。

  • 你有很多选择。当然,您可以只使用 JSON 对象图,甚至可以使用 JSON 文本的原始字符串,但您也可以使用动态对象,这样您就可以在运行时绑定到属性,而无需在编译时定义类。

  • 您还可以使用真正的 C# 对象或所谓的实体,它们可能是您的业务域类。

人际关系

让我们看一下文档的层次结构。它有一些顶级属性,如所需的 id、lastName 和 isRegistered,但它也有嵌套属性。

{ 
   "id": "AndersenFamily", 
   "lastName": "Andersen", 
	
   "parents": [ 
      { "firstName": "Thomas", "relationship": "father" }, 
      { "firstName": "Mary Kay", "relationship": "mother" } 
   ],
	
   "children": [ 
      { 
         "firstName": "Henriette Thaulow", 
         "gender": "female", 
         "grade": 5, 
         "pets": [ { "givenName": "Fluffy", "type": "Rabbit" } ] 
      } 
   ], 
	
   "location": { "state": "WA", "county": "King", "city": "Seattle"}, 
   "isRegistered": true 
}
  • 例如,parents 属性作为 JSON 数组提供,如方括号所示。

  • 我们还有另一个子数组,即使本例中数组中只有一个子数组。这就是在文档中模拟一对多关系的方式。

  • 您只需使用数组,其中数组中的每个元素可以是一个简单值或另一个复杂对象,甚至是另一个数组。

  • 因此,一个家庭可以有多个父母和多个孩子,如果您查看子对象,就会发现它们有一个宠物的属性,该属性本身就是一个嵌套数组,用于孩子和宠物之间的一对多关系。

  • 对于位置属性,我们将三个相关属性(州、县和城市)组合成一个对象。

  • 以这种方式嵌入对象而不是嵌入对象数组类似于关系数据库中单独表中的两行之间具有一对一的关系。

嵌入数据

当您开始对文档存储(例如 DocumentDB)中的数据进行建模时,请尝试将实体视为以 JSON 表示的独立文档。在使用关系数据库时,我们总是对数据进行规范化。

  • 数据标准化通常涉及获取一个实体(例如客户),并将其分解为离散的数据片段,例如联系方式和地址。

  • 要读取客户及其所有联系方式和地址,您需要使用 JOINS 在运行时有效聚合数据。

现在让我们看看如何将相同的数据建模为文档数据库中的独立实体。

{
   "id": "1", 
   "firstName": "Mark", 
   "lastName": "Upston", 
	
   "addresses": [ 
      {             
         "line1": "232 Main Street", 
         "line2": "Unit 1", 
         "city": "Brooklyn", 
         "state": "NY", 
         "zip": 11229
      }
   ],
	
   "contactDetails": [ 
      {"email": "mark.upston@xyz.com"}, 
      {"phone": "+1 356 545-86455", "extension": 5555} 
   ]
} 

正如您所看到的,我们已经对客户记录进行了非规范化处理,其中客户的所有信息都嵌入到单个 JSON 文档中。

在 NoSQL 中,我们有一个免费的模式,因此您也可以添加不同格式的联系方式和地址。在 NoSQL 中,您可以通过单个读取操作从数据库检索客户记录。同样,更新一条记录也是一次写操作。

以下是使用 .Net SDK 创建文档的步骤。

步骤 1 - 实例化 DocumentClient。然后,我们将查询 myfirstdb 数据库,并查询 MyCollection 集合,我们将其存储在这个私有变量集合中,以便在整个类中都可以访问它。

private static async Task CreateDocumentClient() { 
   // Create a new instance of the DocumentClient
	
   using (var client = new DocumentClient(new Uri(EndpointUrl), AuthorizationKey)) { 
      database = client.CreateDatabaseQuery("SELECT * FROM c WHERE c.id =
         'myfirstdb'").AsEnumerable().First(); 
			
      collection = client.CreateDocumentCollectionQuery(database.CollectionsLink,
         "SELECT * FROM c WHERE c.id = 'MyCollection'").AsEnumerable().First();  
			
      await CreateDocuments(client); 
   }

}

步骤 2 - 在 CreateDocuments 任务中创建一些文档。

private async static Task CreateDocuments(DocumentClient client) {
   Console.WriteLine(); 
   Console.WriteLine("**** Create Documents ****"); 
   Console.WriteLine();
	
   dynamic document1Definition = new {
      name = "New Customer 1", address = new { 
         addressType = "Main Office", 
         addressLine1 = "123 Main Street", 
         location = new { 
            city = "Brooklyn", stateProvinceName = "New York"
         }, 
         postalCode = "11229", countryRegionName = "United States" 
      }, 
   };
	
   Document document1 = await CreateDocument(client, document1Definition); 
   Console.WriteLine("Created document {0} from dynamic object", document1.Id); 
   Console.WriteLine(); 
}

第一个文档将从这个动态对象生成。这可能看起来像 JSON,但当然不是。这是 C# 代码,我们正在创建一个真正的 .NET 对象,但没有类定义。相反,属性是根据对象的初始化方式推断出来的。您还可以注意到,我们没有为此文档提供 Id 属性。

步骤 3 - 现在让我们看一下 CreateDocument,它看起来与我们在创建数据库和集合时看到的模式相同。

private async static Task<Document> CreateDocument(DocumentClient client,
   object documentObject) {
   var result = await client.CreateDocumentAsync(collection.SelfLink, documentObject); 
	
   var document = result.Resource; 
   Console.WriteLine("Created new document: {0}\r\n{1}", document.Id, document); 
	
   return result; 
}

步骤 4 - 这次我们调用 CreateDocumentAsync 指定我们要将文档添加到的集合的 SelfLink。我们收到带有资源属性的响应,在本例中,该资源属性代表新文档及其系统生成的属性。

在下面的 CreateDocuments 任务中,我们创建了三个文档。

  • 在第一个文档中,Document 对象是 SDK 中定义的类,它继承自资源,因此它具有所有公共资源属性,但它还包括定义无架构文档本身的动态属性。

private async static Task CreateDocuments(DocumentClient client) {
   Console.WriteLine(); 
   Console.WriteLine("**** Create Documents ****"); 
   Console.WriteLine();
	
   dynamic document1Definition = new {
      name = "New Customer 1", address = new {
         addressType = "Main Office", 
         addressLine1 = "123 Main Street", 
         location = new {
            city = "Brooklyn", stateProvinceName = "New York" 
         }, 
         postalCode = "11229", 
         countryRegionName = "United States" 
      }, 
   };
	
   Document document1 = await CreateDocument(client, document1Definition); 
   Console.WriteLine("Created document {0} from dynamic object", document1.Id); 
   Console.WriteLine();
	
   var document2Definition = @" {
      ""name"": ""New Customer 2"", 
		
      ""address"": { 
         ""addressType"": ""Main Office"", 
         ""addressLine1"": ""123 Main Street"", 
         ""location"": { 
            ""city"": ""Brooklyn"", ""stateProvinceName"": ""New York"" 
         }, 
         ""postalCode"": ""11229"", 
         ""countryRegionName"": ""United States"" 
      } 
   }"; 
	
   Document document2 = await CreateDocument(client, document2Definition); 
   Console.WriteLine("Created document {0} from JSON string", document2.Id);
   Console.WriteLine();
	
   var document3Definition = new Customer {
      Name = "New Customer 3", 
		
      Address = new Address {
         AddressType = "Main Office", 
         AddressLine1 = "123 Main Street", 
         Location = new Location {
            City = "Brooklyn", StateProvinceName = "New York" 
         }, 
         PostalCode = "11229", 
         CountryRegionName = "United States" 
      }, 
   };
	
   Document document3 = await CreateDocument(client, document3Definition); 
   Console.WriteLine("Created document {0} from typed object", document3.Id); 
   Console.WriteLine(); 
}
  • 第二个文档仅适用于原始 JSON 字符串。现在,我们进入 CreateDocument 的重载,它使用 JavaScriptSerializer 将字符串反序列化为对象,然后将其传递给我们用于创建第一个文档的相同 CreateDocument 方法。

  • 在第三个文档中,我们使用了应用程序中定义的 C# 对象 Customer。

让我们看一下这个客户,它有一个 Id 和地址属性,其中地址是一个嵌套对象,它有自己的属性,包括位置,这是另一个嵌套对象。

using Newtonsoft.Json; 

using System; 
using System.Collections.Generic; 
using System.Linq; 
using System.Text; 
using System.Threading.Tasks;

namespace DocumentDBDemo {
 
   public class Customer { 
      [JsonProperty(PropertyName = "id")] 
      public string Id { get; set; }
      // Must be nullable, unless generating unique values for new customers on client  
      [JsonProperty(PropertyName = "name")] 
      public string Name { get; set; }  
      [JsonProperty(PropertyName = "address")] 
      public Address Address { get; set; } 
   }
	
   public class Address {
      [JsonProperty(PropertyName = "addressType")] 
      public string AddressType { get; set; }  
		
      [JsonProperty(PropertyName = "addressLine1")] 
      public string AddressLine1 { get; set; }  
		
      [JsonProperty(PropertyName = "location")] 
      public Location Location { get; set; }  
		
      [JsonProperty(PropertyName = "postalCode")] 
      public string PostalCode { get; set; }  
		
      [JsonProperty(PropertyName = "countryRegionName")] 
      public string CountryRegionName { get; set; } 
   }
	
   public class Location { 
      [JsonProperty(PropertyName = "city")] 
      public string City { get; set; }  
		
      [JsonProperty(PropertyName = "stateProvinceName")]
      public string StateProvinceName { get; set; } 
   } 
}

我们还准备了 JSON 属性,因为我们希望在栅栏两边都保持适当的约定。

因此,我只需创建 New Customer 对象及其嵌套子对象,然后再次调用 CreateDocument。尽管我们的客户对象确实有一个 Id 属性,但我们没有为其提供值,因此 DocumentDB 根据 GUID 生成了一个 Id 属性,就像对前两个文档所做的那样。

编译并执行上述代码后,您将收到以下输出。

**** Create Documents ****  
Created new document: 575882f0-236c-4c3d-81b9-d27780206b2c 
{ 
  "name": "New Customer 1", 
  "address": { 
    "addressType": "Main Office", 
    "addressLine1": "123 Main Street", 
    "location": { 
      "city": "Brooklyn", 
      "stateProvinceName": "New York" 
    }, 
    "postalCode": "11229", 
    "countryRegionName": "United States" 
  }, 
  "id": "575882f0-236c-4c3d-81b9-d27780206b2c", 
  "_rid": "kV5oANVXnwDGPgAAAAAAAA==", 
  "_ts": 1450037545, 
  "_self": "dbs/kV5oAA==/colls/kV5oANVXnwA=/docs/kV5oANVXnwDGPgAAAAAAAA==/", 
  "_etag": "\"00006fce-0000-0000-0000-566dd1290000\"", 
  "_attachments": "attachments/" 
} 
Created document 575882f0-236c-4c3d-81b9-d27780206b2c from dynamic object  
Created new document: 8d7ad239-2148-4fab-901b-17a85d331056 
{ 
  "name": "New Customer 2", 
  "address": {
    "addressType": "Main Office", 
    "addressLine1": "123 Main Street", 
    "location": { 
      "city": "Brooklyn", 
      "stateProvinceName": "New York" 
    }, 
    "postalCode": "11229", 
    "countryRegionName": "United States" 
  }, 
  "id": "8d7ad239-2148-4fab-901b-17a85d331056", 
  "_rid": "kV5oANVXnwDHPgAAAAAAAA==", 
  "_ts": 1450037545, 
  "_self": "dbs/kV5oAA==/colls/kV5oANVXnwA=/docs/kV5oANVXnwDHPgAAAAAAAA==/", 
  "_etag": "\"000070ce-0000-0000-0000-566dd1290000\"", 
  "_attachments": "attachments/" 
} 
Created document 8d7ad239-2148-4fab-901b-17a85d331056 from JSON string  
Created new document: 49f399a8-80c9-4844-ac28-cd1dee689968 
{ 
  "id": "49f399a8-80c9-4844-ac28-cd1dee689968", 
  "name": "New Customer 3", 
  "address": { 
    "addressType": "Main Office", 
    "addressLine1": "123 Main Street", 
    "location": { 
      "city": "Brooklyn", 
      "stateProvinceName": "New York" 
    }, 
    "postalCode": "11229", 
    "countryRegionName": "United States" 
  }, 
  "_rid": "kV5oANVXnwDIPgAAAAAAAA==", 
  "_ts": 1450037546, 
  "_self": "dbs/kV5oAA==/colls/kV5oANVXnwA=/docs/kV5oANVXnwDIPgAAAAAAAA==/", 
  "_etag": "\"000071ce-0000-0000-0000-566dd12a0000\"", 
  "_attachments": "attachments/" 
}
Created document 49f399a8-80c9-4844-ac28-cd1dee689968 from typed object