支持通过分区键查询而无需更改接口的存储库

问题描述

我正在开发一个使用IDocumentClient对CosmosDB进行查询的应用程序。我的GenericRepository支持通过IdPredicate进行查询

数据库sqlServer更改为CosmosDb时遇到麻烦,在CosmosDb中,我们有partition key。而且我不知道如何在不通过更改接口传递partition key作为参数的情况下实现通过partition key支持查询的存储库。

public interface IRepository<T>
{
    //I can handle this one by adding value of partition key to id and split it by ":"
    Task<T> FindByIdAsync(string id);

    // I am stuck here!!!
    Task<T> FindByPredicateAsync(Expression<Func<T,bool>> predicate);
}

我的实现

public class Repository<T> : IRepository<T>
{
    private readonly IDocumentClient _documentClient;

    private readonly string _databaseId;
    private readonly string _collectionId;

    public Repository(IDocumentClient documentClient,string databaseId,string collectionId)
    {
        _documentClient = documentClient;

        _databaseId = databaseId;
        _collectionId = collectionId;
    }

    public async Task<T> FindByIdAsync(string id)
    {
        var documentUri = UriFactory.CreateDocumentUri(_databaseId,_collectionId,id);

        try
        {
            var result = await _documentClient.ReadDocumentAsync<TDocument>(documentUri,new RequestOptions
            {
                PartitionKey = ParsePartitionKey(documentId)
            });

            return result.Document;
        }
        catch (DocumentClientException e)
        {
            if (e.StatusCode == HttpStatusCode.NotFound)
            {
                throw new EntityNotFoundException();
            }

            throw;
        }
    }
    
    public async Task<T> FindByPredicateAsync(Expression<Func<T,bool>> predicate)
    {
         //Need to query CosmosDb with partition key here!
    }

    private PartitionKey ParsePartitionKey(string entityId) => new PartitionKey(entityId.Split(':')[0]);
}

非常感谢您的帮助。

解决方法

我已经找到了使您的存储库独立于数据库的解决方案(例如,我正在使用v3 SDK)。只需将当前界面分为两部分:

public interface IRepository<T>
{
    Task<T> FindItemByDocumentIdAsync(string documentId);

    
    Task<IEnumerable<T>> FindItemsBySqlTextAsync(string sqlQuery);

    Task<IEnumerable<T>> FindAll(Expression<Func<T,bool>> predicate = null);
}

public interface IPartitionSetter<T>
{
    string PartititonKeyValue { get; }

    void SetPartitionKey<T>(string partitionKey);
}//using factory method or DI framework to create same instance for IRepository<T> and IPartitionSetter<T> in a http request

实施:

public class Repository<T> : IRepository<T>,IPartitionSetter<T>
{
    //other implementation

    public async Task<IEnumerable<T>> FindAll(Expression<Func<T,bool>> predicate = null)
    {
        var result = new List<T>();
        var queryOptions = new QueryRequestOptions
        {
            MaxConcurrency = -1,PartitionKey = ParsePartitionKey()
        };

        IQueryable<T> query = _container.GetItemLinqQueryable<T>(requestOptions: queryOptions);

        if (predicate != null)
        {
            query = query.Where(predicate);
        }

        var setIterator = query.ToFeedIterator();
        while (setIterator.HasMoreResults)
        {
            var executer = await setIterator.ReadNextAsync();

            result.AddRange(executer.Resource);
        }

        return result;
    }

    private string _partitionKey;

    public string PartititonKeyValue => _partitionKey;

    private PartitionKey? ParsePartitionKey()
    {
        if (_partitionKey == null)
            return null;
        else if (_partitionKey == string.Empty)
            return PartitionKey.None;//for query documents with partition key is empty
        else
            return new PartitionKey(_partitionKey);
    }

    public void SetPartitionKey<T>(string partitionKey)
    {
        _partitionKey = partitionKey;
    }
}

在执行查询以在此处应用分区键之前,您需要注入IPartitionSetter<T>并调用SetPartitionKey

,

这是您想要的吗?

BaseModel.cs(不需要。仅在使用通用保存/更新时才需要)

public class BaseModel
{
     public int Id { get; set; }
     public DateTime? CreatedDate { get; set; }
     public string CreatedBy { get; set; }
     public DateTime? ModifiedDate { get; set; }
     public string ModifiedBy { get; set; } 
}

User.cs

public class User : BaseModel
{
     public string Name { get; set; }
     public int? Age { get; set; }
}

YourRepository.cs

public Task<T> FindByPredicateAsync(Expression<Func<T,bool>> predicate)
{
     return _context.Set<T>().Where(predicate).FirstOrDefault();
}

YourController.cs

string id = "1:2";
string[] ids = id.Split(":");

Expression<Func<User,bool>> exp = x => ids.Contains(x.Id);
FindByPredicateAsync<User>(exp);
,

似乎您正在尝试使用文档ID的一部分作为FindByIdAsync方法中的分区键。不知道我是否可以遵循该逻辑背后的上下文,或者仅仅是随机尝试。如果您确实没有将实体的其他任何属性用作document ID itself as the partition key,则可以将good partition key用于容器(也称为集合)。

注意:我发现您在上面的示例代码中使用的是较旧的V2 SDK。因此,我在下面的答案中同时提供了V2和较新的V3 SDK示例,以防万一您仍然想继续使用V2。

对于documentClient.ReadDocumentAsync (V2 SDK)调用,不需要分区键,因为您正在按ID进行读取(并且如果分区键本身就是ID)。如果使用V3 SDK container.ReadItemAsync,则可以将id本身作为分区键进行传递,前提是您已如开始时所述选择ID作为分区键。

现在,关于其他方法FindByPredicateAsync,这是一个棘手的情况,因为您的谓词可能是实体任何属性的条件。如果传递分区键,它将仅在同一分区内查询可能与谓词匹配的其他分区中缺少的记录。 Example (V2 SDK)Example (V3 SDK)。因此,一种选择是通过在V2 SDK的情况下将“请求选项”的EnableCrossPartitionQuery属性设置为true来使用跨分区查询,并且不设置分区键。在V3 SDK中,如果未设置QueryRequestOptions的分区键,它将自动启用交叉分区。 注意:请注意跨分区查询的性能和RU成本。

为便于整体参考,这里是Cosmos DB documentation Map