不改变接口支持分区键查询的仓库

Repository that support query by partition key without change interface

我正在开发一个使用 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]);
}

非常感谢任何帮助,谢谢。

这是你想要的吗?

BaseModel.cs(不需要。仅当您使用通用 save/update 时才需要)

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 for your container (aka collection) if you really do not have any other property of the entity to be a good partition key.

注意:我看到您在下面的回答中使用了较旧的 V2 SDK in your sample code above. So I have provided both V2 and the newer V3 SDK 示例,以防您现在仍想坚持使用 V2。

对于 documentClient.ReadDocumentAsync (V2 SDK) call, partition key is not required since you are reading by ID (and if your partition key is the id itself). In case of V3 SDK, container.ReadItemAsync,您可以将 id 本身作为分区键传递,假设您是否像我在开头提到的那样选择了它作为分区键。

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

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

我找到了使您的存储库独立于数据库的解决方案(例如,我使用的是 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 以在此处应用分区键。