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 数组提供,如方括号所示。

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

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

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

  • 对于 location 属性,我们将三个相关的属性(state、county 和 city)组合到一个对象中。

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

嵌入数据

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

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

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

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

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

如您所见,我们已将客户记录反规范化,其中客户的所有信息都嵌入到单个 JSON 文档中。

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

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

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

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。我们获得一个包含 resource 属性的响应,在本例中,该属性表示具有其系统生成属性的新文档。

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

  • 在第一个文档中,Document 对象是 SDK 中定义的一个类,它继承自 resource,因此它具有所有通用的 resource 属性,但它还包括定义无模式文档本身的动态属性。

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 和 address 属性,其中 address 是一个嵌套对象,它有自己的属性,包括 location,它是另一个嵌套对象。

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 生成了一个,就像它对前两个文档所做的那样。

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

**** 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
广告