数据存储

Azure DevOps Services | Azure DevOps Server 2022 - Azure DevOps Server 2019

Azure DevOps 扩展可以直接将用户首选项和复杂的数据结构存储在 Microsoft 提供的基础结构上,这可确保用户的数据像其他组织和项目数据一样安全并备份。 这也意味着,对于简单的数据存储需求,你不需要作为扩展提供程序来设置、管理或支付第三方数据存储服务的费用。

有两种方法可以与数据存储服务互动:通过 REST API 或通过 Microsoft 提供的客户端服务(这是 VSS SDK 的一部分)。 我们建议扩展开发人员利用提供的客户端服务 API,因为它们提供对 REST API 的用户友好封装。

注意

正在查找 Azure DevOps REST API? 请参阅最新的 Azure DevOps REST API 参考

有关 .NET 客户端库的信息,请参阅 适用于 Azure DevOps 的 .NET 客户端库。

可以存储的内容

该服务旨在让你存储和管理两种不同类型的数据:

  • 设置:简单的键值设置(如用户首选项)
  • 文档:类似复杂对象的集合(文档)

集合作为文档的索引容器。 文档是属于集合的 JSON Blob。 除了几个保留的属性名称之外,还可以控制和管理这些文档的架构。

如何确定数据的范围

设置和文档集合的范围可以限定为:

  • 项目集合:由安装扩展的项目集合的所有用户共享
  • 用户:安装扩展的项目集合的单个用户

设置存储

管理设置getValue()的两个主要方法是:setValue()

  • getValue() 接受字符串键(以及其他选项(如范围)并返回 IPromise。 此承诺的解析值是与提供的密钥关联的值。
  • setValue() 接受字符串键、值和其他选项(如范围),并返回 IPromise。 此承诺的已解析值是设置的更新值。

下面是如何设置值的示例:

        private async initializeState(): Promise<void> {
        await SDK.ready();
        const accessToken = await SDK.getAccessToken();
        const extDataService = await SDK.getService<IExtensionDataService>(CommonServiceIds.ExtensionDataService);
        this._dataManager = await extDataService.getExtensionDataManager(SDK.getExtensionContext().id, accessToken);

        this._dataManager.getValue<string>("test-id").then((data) => {
            this.setState({
                dataText: data,
                persistedText: data,
                ready: true
            });
        }, () => {
            this.setState({
                dataText: "",
                ready: true
            });
        });
    }

下面是如何检索设置值的示例:

    // Get data service
    SDK.getService(SDK.getContributionId()).then(function(dataService) {
        // Get value in user scope
        dataService.getValue("userScopedKey", {scopeType: "User"}).then(function(value) {
            console.log("User scoped key value is " + value);
        });
    });

如果未 scopeType 指定,则设置存储在项目集合级别,并且可使用扩展访问该项目集合中的所有用户。 下面是如何在项目集合级别设置设置值的示例:

    // Get data service
    SDK.getService(SDK.getContributionId()).then(function(dataService) {
        // Set value (default is project collection scope)
        dataService.setValue("someKey", "abcd-efgh").then(function(value) {
            console.log("Key value is " + value);
        });
    });

数据(文档集合)存储

若要处理除键值对之外的更复杂的数据,可以利用文档的概念对扩展的数据执行 CRUD 操作。 文档是 JSON Blob,具有两个特殊属性:ID 和 __etag。 如果它们对扩展的数据模型至关重要,则 ID 可以是用户定义的,或者如果未指定,则系统会生成它们。 这些 ID 在特定集合中必须是唯一的。 由于集合引用了扩展的特定范围和实例,因此这意味着可以在不同的集合中重复使用相同的文档 ID。

以下文档操作可用:

  • 获取文档
  • 创建文档
  • 设置文档(创建或更新)
  • 更新文档
  • 删除文档

还可以对集合执行单个操作:获取所有文档

按 ID 获取文档

使用文档标识符从集合中获取文档非常简单,如以下示例所示:

    // Acquire data service
    SDK.getService(SDK.getContributionId()).then(function(dataService) {
        // Retrieve document by id
        dataService.getDocument("MyCollection", "MyDocumentId").then(function(doc) {
            // Assuming document has a property named foo
            console.log("Doc foo: " + doc.foo);
        });
    });

此操作尝试从“MyCollection”集合中提取 ID 为“MyDocumentId”的文档。 如果没有提供的范围,服务默认使用范围限定为此扩展的整个实例的集合。 如果此集合或具有指定 ID 的文档不存在,则返回 404 错误,扩展应处理此错误。 返回的文档是一个 JSON 对象,其中包含其所有属性,以及数据存储服务利用的特殊 ID 和 __etag 属性。

    // Get data service
    SDK.getService(SDK.getContributionId()).then(function(dataService) {
        // Get document by id
        dataService.getDocument("MyCollection", "MyDocumentId").then(function(doc) {
            // Assuming document has a property named foo
            console.log("Doc foo: " + doc.foo);
        });
    });

此调用尝试从集合“MyCollection”检索 ID 为“MyDocumentId”的文档。由于未提供作用域,因此服务使用的集合的范围限定为此扩展的整个实例的默认值。 如果此集合不存在,或者具有该 ID 的文档不存在,则返回 404,该扩展应处理该集合。 返回的文档是包含其所有属性的 JSON 对象,以及数据存储服务使用的特殊 ID 和 __etag 属性。

创建文档

若要创建新文档,请执行如下例所示的调用:

    // Get data service
    SDK.getService(SDK.getContributionId()).then(function(dataService) {
        // Prepare document first
        var newDoc = {
            fullScreen: false,
            screenWidth: 500
        };

        dataService.createDocument("MyCollection", newDoc).then(function(doc) {
            // Even if no ID was passed to createDocument, one gets generated
            console.log("Doc id: " + doc.id);
        });
    });

如果提供的名称和作用域的集合尚不存在,则会在创建文档本身之前动态创建该集合。

如果提供的文档包含属性,该值将用作文档的唯一 id ID。 请注意,提供的 id 字符应限制为 50 个字符。 如果该字段不存在,则由服务生成 GUID,并在解析承诺时返回的文档中包含该 GUID。

如果集合中的另一个文档与文档上提供的文档的 ID 相同,则操作将失败。 如果所需的行为是创建新文档(如果 ID 不存在),但修改现有文档(如果存在), setDocument() 则应使用该方法。

设置文档(更新或创建)

setDocument() 函数执行“upsert”操作 - 如果现有文档的 ID 存在并且与集合中的文档匹配,它将修改现有文档。 如果 ID 不存在或与集合中的任何文档不对应,则将新文档添加到集合中。

    // Get data service
    SDK.getService(SDK.getContributionId()).then(function(dataService) {
        // Prepare document first
        var myDoc = {
            id: 1,
            fullScreen: false,
            screenWidth: 500
        };

        dataService.setDocument("MyCollection", myDoc).then(function(doc) {
            console.log("Doc id: " + doc.id);
        });
    });

更新文档

updateDocument 函数要求正在更改的文档已驻留在集合中。 如果未提供 ID,或者提供的 ID 与集合中的任何文档不对应,则会引发异常。

下面是如何使用更新的示例:

    // Get data service
    SDK.getService(SDK.getContributionId()).then(function(dataService) {
        var collection = "MyCollection";
        var docId = "1234-4567-8910";
        // Get document first
        dataService.getDocument(collection, docId, { scopeType: "User" }).then(function(doc) {
            // Update the document
            doc.name = "John Doe";
            dataService.updateDocument(collection, doc, { scopeType: "User" }).then(function(d) {
                // Check the new version
                console.log("Doc version: " + d.__etag);
            });
        });
    });

删除文档

此函数从提供的集合中删除具有提供的 ID 的文档。 如果集合不存在或文档不存在,则返回 404。

下面是一个示例用法:

    // Get data service
    SDK.getService(SDK.getContributionId()).then(function(dataService) {
        var docId = "1234-4567-8910";
        // Delete document
        dataService.deleteDocument("MyCollection", docId).then(function() {
            console.log("Doc deleted");
        });
    });

获取集合中的所有文档

以下示例使用数据服务从“MyCollection”集合中检索所有文档,然后将文档数记录到控制台:

    // Get data service
    SDK.getService(SDK.getContributionId()).then(function(dataService) {
        // Get all document under the collection
        dataService.getDocuments("MyCollection").then(function(docs) {
            console.log("There are " + docs.length + " in the collection.");
        });
    });

此调用检索范围集合中的所有文档,限制为 100,000 个文档。 如果集合不存在,则返回 404 错误。

高级

如何存储设置

此调用封装 setDocument 客户端方法,并为其提供多个数据片段。 如前所述,设置在内部保存为文档。 因此,将动态生成基本文档,其中文档的 ID 是方法中 setValue() 提供的密钥。 文档还有两个属性。 该 value 属性保存传递给方法的值,并且该 revision 属性设置为 -1。 虽然该revision属性在“使用文档”部分中进行了详细说明,但在设置上下文中,设置为revision-1文档表示我们不关心此设置文档的版本控制。

由于设置存储为文档,因此我们需要提供集合名称,指示文档的存储位置。 为简单起见,使用setValue()/getValue()方法时,集合名称始终是特殊名称。$settings 前面的调用在以下终结点发出 PUT 请求:

GET _apis/ExtensionManagement/InstalledExtensions/{publisherName}/{extensionName}/Data/Scopes/User/Me/Collections/%24settings/Documents

请求有效负载类似于以下示例:

{
                "id": "myKey",
                "__etag": -1,
                "value": "myValue"
}

REST API

假设在设置值后执行此代码段,应会看到一条包含文本“Value is myValue”的警报消息。getValue 方法再次是 REST API 的包装器,向以下终结点发出 GET 请求:

GET _apis/ExtensionManagement/InstalledExtensions/{publisherName}/{extensionName}/Data/Scopes/User/Me/Collections/%24settings/Documents/myKey

etags

数据__etag存储服务使用此字段进行文档并发管理。 保存更新之前,服务会验证当前存储的文档是否 __etag 与更新的文档匹配 __etag 。 如果匹配,则会 __etag 递增更新的文档,并将更新的文档返回到调用方。 如果它们不匹配,则表示要更新的文档已过时,并引发异常。 扩展编写器负责正常处理此异常,方法是检索文档的最新 __etag 内容、合并更改和重试更新,或者通知用户。

对于某些类型的文档,可能不需要提供的并发级别,并且最后一个 wins 模型可能更合适。 在这种情况下,在编辑文档时,输入 -1 作为 __etag 表示此功能的值。 以前提及设置服务使用此模型来存储设置和首选项。