您将数据存储在哪里?

哈Ha! 我们继续进行实验性的系列文章,观察您可以实时影响在UWP上创建游戏的过程。 今天,我们将讨论“在哪里存储数据?”这个问题在开发人员队伍中不断出现。 加入我们,并在评论中分享您的想法!




我请作者Alexei Plotnikov发言

在上一篇文章中,我提出了在设备之间方便地同步用户数据的问题,首先我解决了其标识问题,但这只是实现目标所要做的工作的最小部分。

一个更复杂的问题是用户数据的位置,最重要的是,在Microsoft的努力下,当提出这样一个问题时,首先想到的是Microsoft Azure 。 Azure云平台提供了广泛的服务,似乎没有任何任务无法借助其帮助解决。 无论是真的还是假的,我都无法判断,但是我的任务肯定在这个平台的力量之内。 但是,首先是第一件事。

我们从小开始-什么是云? 我在2012年第一次听说云,然后最常使用“云计算”这一短语。 这种计算的最初想法是将计算工作分配在彼此远离的不同设备之间。 关于未来的演讲尤其令人印象深刻,由于计算将在全球所有计算机之间进行分配,因此即使是最困难的任务也将在短时间内处理。

在实践中,所有这些都归因于分布在世界各地的数据中心并向消费者提供其计算能力,而最初的概念只是数据中心内部机器之间以及数据中心本身之间的分布(通常是在同一地区)。

基于上述内容,我们可以假定,当您听到“云”一词时,您可以将其视为更熟悉的“主机”,唯一的区别是无需您付出任何额外的努力就可以扩展云的功能。

您可能有的第二个问题是为什么选择Azure? “因为这是Microsoft博客上的文章,所以作者只会谈论他们的产品。”-您说,这是错误的。 使用Azure的动机更为普遍-由于这是Microsoft产品,因此在开发我的应用程序的帮助下,它与其他产品的集成度最高。

但是,我注意到该公司尽一切努力使Azure吸引Android或iOS开发人员。 好吧,最后但并非最不重要的问题是使用云的成本。 由于我是BizSpark订阅的持有人,因此我获得了每月包包贷款的利息,这笔利息远远超过了我的云需求,尽管免费提供的条件也可以满足私人开发人员的大部分需求。

现在,让我们继续直接选择同步和数据存储机制。 我不会变得狡猾,作为一个自学成才的人,我经常不得不面对我不了解的技术,然后才了解UWP,我使用SQL数据库解决了类似的问题。

但是,UWP没有使用经典SQL DBMS的方法,并且提供了SQLite作为替代,并且在开始研究后,我发现这种数据库是内置的,适合于方便存储和使用本地数据,但绝对不适合放置数据在远程存储中。 在撰写本文的过程中,选择了正确的技术后,我遇到了移动应用程序开发领域中的一种Azure解决方案,该解决方案允许您在设备之间同步SQLite表中的数据,但是在仔细考虑之后,我仍然是最初的选择。

顺便说一下,做出初始选择并不困难,因为Microsoft礼貌地提示了UWP开发人员可能必须面对的技术列表。 在最新版本的Visual Studio中,创建新的UWP项目时,您将看到一个页面,其中包含有关入门的建议,其中一个链接显示为“添加推荐的服务”。 通过单击此链接,将打开“连接服务”选项卡,并且已经在其中显示了选项“带有Azure存储服务的云存储”。



直觉表明这正是您所需要的,因此我决定专注于对此问题的深入研究,以期在项目中进一步使用。

云存储是一组用于不同任务的产品,可以在这里阅读更多内容,但是我主要对表存储感兴趣,表存储原来是NoSQL数据库。

NoSQL是一种无模式的数据库,即无需在其中预先构造表的数据库。 实际上,这种情况下的表只是该节的路径的一部分,这意味着单个表可以包含行,例如一次包含三列和五列。 为了完全理解表存储的功能,建议您仔细阅读该手册 ,但我将从平凡的角度考虑该主题,因为最终该材料面向的是初学者,我是本主题的读者。

首先,让我们弄清楚如何创建NoSQL表:

1.注册一个免费的 Azure帐户。 如果您具有MSDN或BizSpark订阅,并且已在Azure中激活,则可以跳过此步骤。 免费帐户在第一个月为您提供了200美元的贷款,然后免费访问大多数Azure服务的一定数量的资源。 译成一种可以理解的语言,一切都以这种方式进行,您无需支付任何费用,直到产品的收入足以支付费用为止,更不用说使用Azure进行自我教育了。

但是,即使您超过了免费门槛,表存储的价格也比类似数量的SQL数据库的价格更为忠诚。 例如,在撰写本文时,我已经创建了两个表,到目前为止只有一个条目。 在报告期的18天内,我平均每天向她求助20次,在此期间从信用帐户中注销了2戈比。 将这些成本按计划的数量增加,我意识到这些成本已超出了应用程序的潜在收入。

2.现在您已经在Azure中拥有一个帐户,我们将继续创建一个存储帐户。



您可以从我上面描述的同一Visual Studio Services连接页面上完成所有操作。 如果您突然关闭了此页面,则可以通过在解决方案资源管理器中双击“ Connected Services”来打开它。 选择所需的服务后,将打开一个包含可用存储帐户的窗口,然后单击相应的按钮以添加一个新的存储帐户。

在新窗口中,您将需要执行以下步骤:

  • 首先,您需要使用Microsoft帐户登录。 您需要使用与订阅绑定的帐户或免费的Azure帐户。
  • 登录帐户后,您的订阅(a)将显示在“订阅”字段中。 一切都是简单的选择,因此注释是多余的。
  • 在“名称”字段中,指定所需的存储服务名称。 由于这也是服务的域名,因此它在Azure中所有可用帐户中必须唯一,而不仅仅是您自己的帐户。
  • “价格类别”字段将要求您了解云平台与传统托管之间的区别,因为通过单击该字段下方的链接,您可以看到价格列表,但没有清晰的解释,它为您提供了每种选择。 当然,在大量的附加链接中,您可以找到有关所有这些缩写的全面信息,例如GRS和LRS,但这对于一般开发人员来说是多余的。 足以理解的是,资费越昂贵,数据处理和存储中涉及的数据中心就越多,其安全性的可能性就越高。 对于小型项目,最低的LRS率很好。
  • “资源组”是针对单个管理的多个Azure服务的组合。 在我们的例子中,创建一个新的,分配任何友好的名称,然后继续。
  • 最后要选择的是服务的“位置”。 位置是指负责处理我们的数据的数据中心的实际位置。 请注意,我说的是复数形式,因为在一个区域中可以有多个数据中心,并且可以在它们之间分配工作(如果您选择建议价的类别)。 选择与您的主要用户群最接近的一个。 但是,如果您打算扩展到整个世界,并且需要在世界任何地方提供最大的响应,那么没人会为应用程序的每个区域版本而烦恼,以创建一个单独的存储帐户并实现它们之间的数据同步。 高可扩展性是云的主要优势。

3.创建存储帐户后,它将被添加到列表中,您可以通过单击“添加”按钮继续。 该操作的结果将是将用于Azure的NuGet包添加到项目中,并将连接字符串保存在项目的app.config文件中。

不幸的是,无法在UWP中使用此文件中的值(或可能使用可怕的拐杖),因此只需将连接字符串从那里复制到存储服务中,再复制到项目中的方便位置,然后继续下一步。

4.现在剩下的工作就是创建表并开始使用它。 从这里开始,直接取决于任务的个人工作。

事实是,在开始创建任何表之前,您应该仔细考虑用于存储数据的体系结构。 使用表存储非常方便,直接从代码创建新表仅需几行,并且很自然地希望为每个用户分配一个单独的表,因为最终的任务是在他的设备之间同步数据。 但是,在使用不熟悉的技术时,您不应仓促做出决定,而需要仔细权衡利弊。 手册中的特殊文章可以帮助您做出正确的决定,但要准备多次重新阅读它,因为很难立即学习所有数据,尤其是考虑到大量新术语。

考虑到您仍然阅读手册并了解使用表存储的某些功能,我将继续讲故事。 例如,我意识到,从概念上讲,表不是某种孤立的单元,而是逻辑上对记录进行分组的地方。 如果将表显示为存储数据文件的文件夹,这将很容易理解。 文件夹本身不占用空间,也不是文件的组成部分,而只是定义了逻辑上但不是必须保存在此文件夹中的文件路径的一部分。

由此得出的结论很简单-没有人会费心将所有用户的设置存储在一个表中,主要的是,PartitionKey和RowKey列中的一对值在表中是唯一的。 这在我的项目中再次实现,因为用户ID将充当PartitionKey,例如字符串“ UserName”将成为RowKey,这将使我们能够确定存储用户名的唯一记录。 但是正如我上面所说,我们需要权衡所有利弊,所以让我们权衡一下:

  • 为每个用户的数据“单独”存储一个表,可以方便地感知数据结构。 如果我们将表格作为包含文件的文件夹,那么合乎逻辑的是,一个用户的所有文件都在同一个文件夹中,使用这种架构更为常见。
  • 所有其他因素都“反对”一个单独的表格。 用户数据位于单独的表中-在此类表的数量达到数千之前,这是很方便的准确方法。 由于存储帐户在表上方,因此没有其他分组。

    鉴于潜在的用户基础,我们冒着淹没成千上万张单独表的风险,失去了那些具有任何优先级值的表。 同时,将所有用户的设置存储在一个表中可简化管理并使用数据来收集统计信息或实现社交功能。

    此外,使用表存储的低成本使您可以根据所需逻辑在单独的表中复制任何数据。 特别是,我计划创建一个附加表,其中包含用户名,图形表达的链接和属于国家的指示,这些表将用于对表或可能添加到应用程序中的其他社交功能进行评分。

因此,当您确定了数据存储结构后,让我们最后添加一个新表。 由于我们拒绝在代码级别创建它,因此保留了两个选择:通过Azure Web门户或使用特殊的Microsoft Azure Storage Explorer工具,可以从storageexplorer.com下载该工具。 在这两种情况下,都必须选择所需的存储帐户,然后在“表/表服务”部分中选择“ +表/创建表”。 在出现的对话框中,输入所需的名称并提交更改。



之后,您可以从代码中使用新表,而不会出现任何问题。

我将对表执行的主要操作是行的插入和提取,这在表存储术语中称为“实体”。 当您意识到要插入和检索实体时,需要将其映射为从Microsoft.WindowsAzure.Storage.Table的TableEntity继承的类,这样的术语更易于理解。 后继类将已经包含一些必填字段,例如PartitionKey(节名)和RowKey(行名),而我们独立实现的那些字段将是行中的列(实体属性)。

考虑一个表格示例,其中将存储所有球员的名单以及他们的姓名,头像和国家/地区。

    : Imports Microsoft.WindowsAzure.Storage Imports Microsoft.WindowsAzure.Storage.Table 

我决定将用于表的方法放在一个单独的类中,以方便从应用程序的不同角度进行工作。 创建它并立即添加先前已知的常量:

 Public Class AzureWorker Private Const AzureStorageConnectionString As String = "  ,     app.config" Private Const GamerListTableNameString As String = "GamerList" '    … End Class 

现在我们需要创建一个类,该类将映射到表内的实体(行):

  Private Class GamerListClodTableDataClass Inherits TableEntity Public Const RowKeyValue As String = "UserID" Public Sub New () RowKey = RowKeyValue End Sub Public Property UserName As String = "" Public Property UserountryID As String = "" Public Property UserAvatar As String = "" End Class 

要映射的类必须从TableEntity继承,并且具有我们计划放置在表中的数据字段。 请注意,不必在类级别设置RowKey或PartitionKey的值,但在我的情况下,由于无论其他输入如何,RowKey都是不可变的,因此已设置。

但是,由于在此阶段您可能尚未完全了解使用表存储的本质,因此我将解释在此阶段确定的逻辑。 处理表的最快方法是通过字符串名称和节名称查询实体,因此您需要提前了解此数据。 此外,PartitionKey和RowKey的组合在表中必须是唯一的,这意味着在其中一个键中写入唯一的用户ID,并为第二个键分配我们将永远知道的任何名称是合乎逻辑的。 这正是在GamerListClodTableDataClass类中完成的。

直接查询表之前的最后准备阶段是在单独的函数中创建其对象:

  Private Shared Function GetCloudTable(tableName As String) As CloudTable Dim storageAccount As CloudStorageAccount = CloudStorageAccount.Parse(AzureStorageConnectionString) Dim tableClient As CloudTableClient = storageAccount.CreateCloudTableClient() Dim table As CloudTable = tableClient.GetTableReference(tableName) Return table End Function 

这样做是为了避免每次我们要向表读取或写入数据时都重复代码。 请注意,该代码不会直接向云发出请求,并且在没有连接的情况下会毫无问题地执行。 他所做的只是根据现有数据(例如存储连接字符串和表名称)逐步创建表对象。

最后,让我们继续直接使用表并从保存当前用户数据开始:

  Public Shared Async Function SavedOrUpdateUserData(u As UserManager) As Task(Of Boolean) Dim table As CloudTable = GetCloudTable(GamerListTableNameString) Try If Await table.ExistsAsync Then Dim UserDataClodTableData As New GamerListClodTableDataClass With {.PartitionKey = u.UserId, .UserName = u.UserName.Trim, .UserountryID = u.UserountryID, .UserAvatar = "https://apis.live.net/v5.0/" & u.UserId & "/picture"} Dim insertOperation As TableOperation = TableOperation.InsertOrReplace(UserDataClodTableData) Await table.ExecuteAsync(insertOperation) Return True End If Catch ex As Exception End Try Return False End Function 

该请求是作为异步函数发出的,因此调用代码可以获得执行结果(成功则为True,失败则为False)。 另外,还将UserManager类型的参数传递给函数,该参数是对带有用户数据的类的引用。 我们在上一篇文章中创建了这样的类,唯一的区别是在此版本中,有一个UserountryID字段,用于存储有关用户所在国家/地区的数据。

对于表查询,首先需要使用存储库的连接字符串和表名称创建其对象(我们之前已将此过程放入单独的函数中)。 接下来,您应该检查表的存在,尽管我们确定我们有一个具有该名称的表,但是可能会发生错误,例如,由于缺乏网络连接或由于云故障(这就是为什么将此代码放置在Try /块中的原因)赶上)。 然后,在写入表之前,您需要创建UserDataClodTableData类的实例,并将所需的值分配给其字段,然后再创建InsertOrReplace操作。 从操作名称可以猜到,如果表中不存在具有相同PartitionKey和RowKey对的行,它将在表中插入新行;如果该行已存在,则将替换数据。 好吧,实际上,最终ExecuteAsync团队将在表存储端执行计划的操作。

从表中读取数据与写入数据一样容易。 例如,要求一个用户名:

  Public Shared Async Function GetUserName(id As String) As Task(Of String) Dim table As CloudTable = GetCloudTable(GamerListTableNameString) Try If Await table.ExistsAsync Then Dim retrieveOperation As TableOperation = TableOperation.Retrieve(Of GamerListClodTableDataClass)(id, GamerListClodTableDataClass.RowKeyValue) Dim retrievedResult As TableResult = Await table.ExecuteAsync(retrieveOperation) If retrievedResult.Result IsNot Nothing Then Return CType(retrievedResult.Result, GamerListClodTableDataClass).UserName End If End If Catch ex As Exception End Try Return "" End Function 

这段代码几乎与之前的代码没有什么不同,还从创建一个表对象并检查其存在开始。 此外,与记录中一样,我们创建一个操作,但是这次是提取操作,需要指示PartitionKey和RowKey。 之后,我们使用ExecuteAsync提取结果,并使用TableResult类型的结果对象,实际上归结为将Result属性转换为要映射的类的类型并提取用户名。

使用表不仅限于读写操作,还支持许多不同的脚本。 例如,您可以创建一个查询来提取具有指定PartitionKey的所有实体或具有指定字段的所有实体,但是记住此类操作的速度以及将通过网络传输的数据量非常重要。

从查询速度的角度来看,上面的示例是最佳的,因为寻址系统很可能会沿着路径“存储名\表名\ PartitionKey + RowKey”找到一个实体,但是,如果只获得一个名称,则会将整个实体作为一个整体加载,这是无益的传输的数据量。

以下是考虑到最大查询优化的修改后的功能代码:

  Public Shared Async Function GetUserName(id As String) As Task(Of String) Dim table As CloudTable = GetCloudTable(GamerListTableNameString) Try If Await table.ExistsAsync Then Dim projectionQuery As TableQuery(Of DynamicTableEntity) = New TableQuery(Of DynamicTableEntity)().Where(TableQuery.CombineFilters(TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, id), "and", TableQuery.GenerateFilterCondition("RowKey", QueryComparisons.Equal, GamerListClodTableDataClass.RowKeyValue))).Select({"UserName"}) Dim resolver As EntityResolver(Of String) = Function(pk, rk, ts, props, etag) Return props("UserName").StringValue End Function Dim result As TableQuerySegment(Of String) = Await table.ExecuteQuerySegmentedAsync(projectionQuery, resolver, Nothing) If result.Count > 0 Then Return result(0) End If End If Catch ex As Exception End Try Return "" End Function 

在此代码中,我们没有创建操作对象,而是创建了一个请求对象,该请求对象包含用于确定需要获得什么的几种方法。 Where方法创建一个筛选器,该筛选器指示仅需要返回PartitionKey和RowKey等于指定值的那些行,下一个Select指示仅需要选择UserName列。

对于此类查询,将结果与任何类进行比较是没有意义的,因此,将IDictionary用作返回值,其中键是列的名称,而值是其内容。 由于函数Exec​​uteQuerySegmentedAsync不知道将获得其执行结果,因此有可能(在这种情况下是必要的)传递EntityResolver委托,该委托是指从字典中获取所需值的函数。 所有这些操作的结果将成为TableQuerySegment,该表将存储所请求用户的名称的第一个索引。

通常,使用查询而不是基本的提取操作可​​以使您显着扩展使用表的可能性,但是要小心,因为与经典SQL不同,此处查询的处理速度直接取决于其参数。 没有人会费心执行查询来检索名称与给定名称相等的所有用户记录,但是这样的查询将比SQL中的对应查询长。 要了解这一点,我再次引向我上面提到的表设计指南,并且我还建议您阅读本文 ,其中提供了有关使用表存储的示例。

重要! 链接文章使用经典.NET应用程序的代码,并且与UWP实现不同。 幸运的是,这种区别并不明显,并且类似物很直观(大多数情况下是在Async前缀中)。

最后,我将分享当前在项目中使用Azure存储的结果。 刚开始时,在收到用户ID并从实时ID下载数据后,我建议他选择一个别名(昵称),以防个人资料中存储的名称不适合他。 然后,将输入的昵称保存在UserManager类中,而不是标准的昵称,所有这些数据都保存在GamerList表中。 在下一次启动时,在后台接收用户ID,并从商店请求别名。 结果,用户在游戏中看到了他的昵称,而不是标准配置文件中的名字。

同样在将来,带有用户列表的表格将很方便将社交功能输入到游戏中,现在,我已经为该数据提出了至少一个应用程序。 在执行此任务时,Azure工具(例如队列存储和Azure Functions)将再次为我提供帮助,但是我将在以下文章之一中对此进行讨论。

Source: https://habr.com/ru/post/zh-CN414261/


All Articles