数据存储

Azure DevOps Services |Azure DevOps Server |Azure DevOps Server 2022 |Azure DevOps Server 2020

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 表示此功能的值。 前面提到的设置服务使用此模型来存储设置和首选项。