Cosmos DB - используйте ARRAY_CONTAINS в LINQ - PullRequest
0 голосов
/ 19 сентября 2018

У меня есть коллекция документов в БД Космос.Документ может иметь внутренний массив объектов.Итак, модель выглядит следующим образом:

public class Document
{
    public string Id { get; set; }

    public IList<InnerDocument> InnerDocuments { get; set; } 
}

public class InnerDocument
{
    public string Type { get; set; }

    public string Created { get; set; }
}

Мне нужно получить все внутренние документы, если хотя бы один из них имеет определенный тип.

Если я создаю запрос следующим образом:

var innerDocument = new InnerDocument()
{
    Type = "foo"
};

context.CreateDocumentQuery<Document>(uri, feedOptions)
    .Where(d => d.id == "sample" && d.InnerDocuments.Contains(innerDocument));

это переводится так:

SELECT * FROM root 
WHERE (root[\"id\"] = "sample" 
    AND ARRAY_CONTAINS(root[\"innerDocuments\"], {\"type\":\"foo\"}))

но ничего не возвращает, потому что ни один внутренний документ не выглядит так (все внутренние документы также созданы), поэтому мне нужно добавить третий параметр в ARRAY_CONTAINS (который сообщаетэтого совпадения только в документе достаточно), поэтому он должен выглядеть следующим образом:

SELECT * FROM root 
WHERE (root[\"id\"] = "sample" 
    AND ARRAY_CONTAINS(root[\"innerDocuments\"], {\"type\":\"foo\"}, true))

Моя проблема в том, что я не понял, как передать третий параметр в linq.Я также попытался написать IEqualityComparer, который всегда возвращает true, но безрезультатно (хорошо, что я получил исключение ..).

У вас есть идея, как я могу передать этот параметр в linq?

Спасибо.

Ответы [ 3 ]

0 голосов
/ 27 ноября 2018

Если я правильно понимаю, вы хотите получить все документы, в которых есть какой-либо внутренний документ в массиве с заданным значением свойства ("foo" в этом примере).

Обычно вы используете .Where(d => d.InnerDocuments.Any(i => i.Type == "foo")), но Any пока не поддерживается поставщиком Cosmos LINQ.

Вместо этого вы можете использовать эту конструкцию в качестве обходного пути:

context.CreateDocumentQuery<Document>(uri, feedOptions)
    .Where(d => d.Id == "sample")
    .SelectMany(d => d.InnerDocuments.Where(i => i.Type == "foo").Select(i => d));

Согласно этой теме Microsoft недавно начала работу над реальной функцией Any для поставщика Cosmos LINQ.

0 голосов
/ 28 ноября 2018

Мое решение было немного более взломанным, чем решение, но оно работает временно, пока не будет реализована полная функциональность для .Any ().

Я использую выражения для динамического построения предиката Where для моих документов, что позволяетя передаю объект CosmosSearchCriteria, который имеет список объектов CosmosCriteria, как показано ниже:

public class CosmosCriteria 
{

    public CosmosCriteria()
    {
        ContainsValues = new List<string>();
    }
    public CosmosCriteriaType CriteriaType { get; set; }
    public string PropertyName { get; set; }
    public string PropertyValue { get; set; }
    public ConvertedRuleComparitor Comparitor { get; set; }
    public DateRange Dates { get; set; }
    public List<string> ContainsValues { get; set; }
}

Это позволяет мне запрашивать любое свойство модели Contact, по существу передавая PropertyName и PropertyValue.

Я не изучал другой обходной путь здесь, чтобы посмотреть, смогу ли я заставить его работать с моим построением дерева выражений, в ту минуту, когда я не могу позволить себе время исследовать.

    public async Task<CosmosSearchResponse<Model.Contact>> 
    GetContactsBySearchCriteriaAsync(int pageSize, long companyId, 
    CosmosSearchCriteria searchCriteria, string continuationToken = null)
    {
        var collectionName = CreateCollectionName(companyId, Constants.CollectionType.Contacts);
        var feedOptions = new FeedOptions { MaxItemCount = pageSize };
        if (!String.IsNullOrEmpty(continuationToken))
        {
            feedOptions.RequestContinuation = continuationToken;
        }
        var collection = UriFactory.CreateDocumentCollectionUri(
                Configuration.GetValue<string>(Constants.Settings.COSMOS_DATABASE_SETTING),
                collectionName);

        IOrderedQueryable<Model.Contact> documents = Client.CreateDocumentQuery<Model.Contact>(
            collection,
            feedOptions
        );

        documents = (IOrderedQueryable<Model.Contact>)documents.Where(document => document.deleted != true);
        bool requiresConcatenation = false;
        foreach (var criteria in searchCriteria.Criteria)
        {
            switch (criteria.CriteriaType)
            {
                case Constants.CosmosCriteriaType.ContactProperty:
                    // This is where predicates for the documents.Where(xxxx) 
                    // clauses are built dynamically with Expressions.
                    documents = AddContactPropertyClauses(documents, criteria);
                    break;
                case Constants.CosmosCriteriaType.PushCampaignHistory:
                    requiresConcatenation = true;
                    break;
            }
        }

        documents = (IOrderedQueryable<Model.Contact>)documents.AsDocumentQuery();

        /*
            From this point onwards, we have to do some wizardry to get around the fact that there is no Linq to SQL
            extension overload for the Cosmos DB function ARRAY_CONTAINS (<arr_expr>, <expr> , bool_expr).
            The feature is planned for development but is not yet ready. 
            Keep an eye on the following for updates:
                /11171089/cosmos-db-ispolzuite-arraycontains-v-linq
                https://feedback.azure.com/forums/263030-azure-cosmos-db/suggestions/11503872-support-linq-any-or-where-for-child-object-collect
        */
        if (requiresConcatenation)
        {
            var sqlString = documents.ToString();
            var jsonDoc = JsonConvert.DeserializeObject<dynamic>(sqlString); // Have to do this to remove the escaping
            var q = (string)jsonDoc.query;
            var queryRootAlias = Util.GetAliasNameFromQuery(q);

            if (queryRootAlias == string.Empty)
            {
                throw new FormatException("Unable to parse root alias from query.");
            }

            foreach (var criteria in searchCriteria.Criteria)
            {
                switch (criteria.CriteriaType)
                {
                    case Constants.CosmosCriteriaType.PushCampaignHistory:
                        q += string.Format(" AND ARRAY_CONTAINS({0}[\"CampaignHistory\"], {{\"CampaignType\":1,\"CampaignId\":{1}, \"IsOpened\": true }}, true) ", queryRootAlias, criteria.PropertyValue);
                        break;
                }
            }

            documents = (IOrderedQueryable<Model.Contact>)Client.CreateDocumentQuery<Model.Contact>(
                collection,
                q,
                feedOptions
            ).AsDocumentQuery();
        }

        var returnValue = new CosmosSearchResponse<Model.Contact>();
        returnValue.Results = new List<Model.Contact>();

        Console.WriteLine(documents.ToString());

        var resultsPage = await ((IDocumentQuery<Model.Contact>)documents).ExecuteNextAsync<Model.Contact>();
        returnValue.Results.AddRange(resultsPage);
        if (((IDocumentQuery<Model.Contact>)documents).HasMoreResults)
        {
            returnValue.ContinuationToken = resultsPage.ResponseContinuation;
        }

        return returnValue;
    }  

Надеюсь, этопоможет, или если у кого-то есть способ получше, пожалуйста, скажите!

Дэйв

0 голосов
/ 20 сентября 2018

Насколько я знаю, к сожалению, нет эквивалента LINQ для перегрузки ARRAY_CONTAINS (<arr_expr>, <expr> , bool_expr).Для реализации ваших сценариев, сейчас вы можете использовать SQL-запрос.В настоящее время мы работаем над набором изменений, которые позволят LINQ для этого сценария.

...