9

I have a hierarchical category document, like parent - Children - Children and so on....

{
    id: 1,
    value: {

    }Children: [{
        id: 2,
        value: {

        }Children: [{
            id: 3,
            value: {

            }Children: [{
                id: 4,
                value: {

                }Children: [{
                    id: 5,
                    value: {

                    }Children: [{
                        id: 6,
                        value: {

                        }Children: [{
                            id: 7,
                            value: {

                            }Children: []
                        }]
                    }]
                }]
            }]
        }]
    }]
}

In such documents, using MongoDB C# driver, how can I find a node where Id = x

I tried something like this

var filter = Builders<Type>.Filter.Eq(x => x.Id, 3);
                var node = mongoDbRepository.GetOne(filter) ??
                             mongoDbRepository.GetOne(Builders<Type>.Filter.Where(x => x.Children.Any(c=>c.Id == 3)));

But this covers only two levels. In my example, I have 7 levels and I don't have a restriction on depth of level

Once I find that node I need to update that node.

MongoDB Documentation talks about hierarchical documents, but doesn't cover my scenario.

HaBo
  • 13,999
  • 36
  • 114
  • 206
  • 1
    Isn't this going against everything a nosql, document-orientated database is designed for? – Darren Wainwright Oct 26 '16 at 17:51
  • @Darren How is it? NoSQL is supposed to be nested documents instead of key references. – HaBo Oct 26 '16 at 18:15
  • @HaBo Did you think about using recursion? – rbr94 Nov 03 '16 at 11:03
  • What is the definition of GetOne? – ntohl Nov 03 '16 at 11:15
  • @ntohl that's just abstract over MongoDB C# API for FIND method – HaBo Nov 03 '16 at 12:33
  • @rbr94 can you please elaborate how you do find and update with recursion uisn MongoDB C# API – HaBo Nov 03 '16 at 12:34
  • This is easy to do in Linq to objects with a recursive query. But that means you would be pulling every document out of MongoDB and then querying in memory. Is that feasible? – Jeffrey Patterson Nov 03 '16 at 12:59
  • @JeffreyPatterson that doesn't sound right. we are looking at single document with nested documents. I think to some extent finding the nested document is not a problem, but updating it without delete and add its uppermost parent seems troublesome. – HaBo Nov 03 '16 at 13:37

3 Answers3

3

In your situation if you

don't have a restriction on depth of level

you can`t create update query. You must change schema for store data:

https://docs.mongodb.com/v3.2/tutorial/model-tree-structures/

If you depth is fixed:

public class TestEntity
{
    public int Id { get; set; }
    public TestEntity[] Value { get; set; }
}

class Program
{
    static void Main()
    {
        const string connectionString = "mongodb://localhost:27017";
        var client = new MongoClient(connectionString);

        var db = client.GetDatabase("TestEntities");
        var collection = db.GetCollection<TestEntity>("Entities");

        collection.InsertOne(CreateTestEntity(1, CreateTestEntity(2, CreateTestEntity(3, CreateTestEntity(4)))));
        const int selctedId = 3;
        var update = Builders<TestEntity>.Update.AddToSet(x => x.Value, CreateTestEntity(9));

        var depth1 = Builders<TestEntity>.Filter.Eq(x => x.Id, selctedId);
        var depth2 = Builders<TestEntity>.Filter.Where(x => x.Value.Any(item => item.Id == selctedId));
        var depth3 = Builders<TestEntity>.Filter.Where(x => x.Value.Any(item => item.Value.Any(item2 => item2.Id == selctedId)));

        var filter = depth1 | depth2 | depth3;

        collection.UpdateMany(filter, update);

        // if you need update document on same depth that you match it in query (for example 3 as selctedId), 
        // you must make 2 query (bad approach, better way is change schema):
        //var testEntity = collection.FindSync(filter).First();
        //testEntity.Value[0].Value[0].Value = new[] {CreateTestEntity(9)}; //todo you must calculate depth what you need in C#
        //collection.ReplaceOne(filter, testEntity);
    }

    private static TestEntity CreateTestEntity(int id, params TestEntity[] values)
    {
        return new TestEntity { Id = id, Value = values };
    }
}
Dmitrii Zyrianov
  • 2,208
  • 1
  • 21
  • 27
  • Lets say I will implement depth to 4 levels, then how can I do the update? – HaBo Nov 05 '16 at 06:44
  • Thanks, this looks cool, I am gonna try this shortly. Just extending my question, if I have to move a node (basically change the parent), how would I go about that? Right now, I do two queries, one for find old parent and delete child, next one find new parent and add child. Wondering if there is a cleaner way with one query. – HaBo Nov 06 '16 at 13:00
  • I don`t know how you may make what you want in one query. Two queries are ok. – Dmitrii Zyrianov Nov 06 '16 at 13:55
0

There seems to be something wrong in your example document. If the parent has 3 fileds: _id ,id and value, the example should be

{
"_id" : ObjectId("581bce9064989cce81f2b0c1"),
"id" : 1,
"value" : {
    "Children" : [ 
        {
            "id" : 2,
            "value" : {
                "Children" : [ 
                    {
                        "id" : 3,
                        "value" : {
                            "Children" : [ 
                                {
                                    "id" : 4,
                                    "value" : {
                                        "Children" : [ 
                                            {
                                                "id" : 5,
                                                "value" : {
                                                    "Children" : [ 
                                                        {
                                                            "id" : 6,
                                                            "value" : {
                                                                "Children" : [ 
                                                                    {
                                                                        "id" : 7,
                                                                        "value" : {
                                                                            "Children" : []
                                                                        }
                                                                    }
                                                                ]
                                                            }
                                                        }
                                                    ]
                                                }
                                            }
                                        ]
                                    }
                                }
                            ]
                        }
                    }
                ]
            }
        }
    ]
}

} Please try the following function, where target is the document and x is the id number you want to update.

    bool FindAndUpdate2(BsonDocument target, int x)
    {
        BsonValue id, children;
        while (true)
        {
            try
            {
                if (target.TryGetValue("_id", out children))
                {
                    id = target.GetValue(1);
                    children = target.GetValue(3);
                }
                else
                {
                    id = target.GetValue(0);
                    children = target.GetValue(2);
                }
                if (id.ToInt32() == x)
                {
                    Update(target);  //change as you like
                    return true;   //success
                }
                else
                    target = children[0] as BsonDocument;
            }
            catch (Exception ex)
            {
                return false;   //failed
            }
        }
    }

else if the parent has 4 fileds: _id ,id, value and children the example should be

{
"_id" : ObjectId("581bdd3764989cce81f2b0c2"),
"id" : 1,
"value" : {},
"Children" : [ 
    {
        "id" : 2,
        "value" : {},
        "Children" : [ 
            {
                "id" : 3,
                "value" : {},
                "Children" : [ 
                    {
                        "id" : 4,
                        "value" : {},
                        "Children" : [ 
                            {
                                "id" : 5,
                                "value" : {},
                                "Children" : [ 
                                    {
                                        "id" : 6,
                                        "value" : {},
                                        "Children" : [ 
                                            {
                                                "id" : 7,
                                                "value" : {},
                                                "Children" : []
                                            }
                                        ]
                                    }
                                ]
                            }
                        ]
                    }
                ]
            }
        ]
    }
]

} Then you can try this:

    bool FindAndUpdate2(BsonDocument target, int x)
    {
        BsonValue id, children;
        while (true)
        {
            try
            {
                if (target.TryGetValue("_id", out children))
                {
                    id = target.GetValue(1);
                    children = target.GetValue(3);
                }
                else
                {
                    id = target.GetValue(0);
                    children = target.GetValue(2);

                }
                if (id.ToInt32() == x)
                {
                    Update(target);  //change as you like
                    return true;   //success
                }
                else
                    target = children[0] as BsonDocument;
            }
            catch (Exception ex)
            {
                return false;   //failed
            }
        }
    }
  • This is my first time to post answer – CHENGLIANG YE Nov 04 '16 at 05:15
  • Please avoid posting unnecessary comments which are not related to Question/Answer. – mmushtaq Nov 04 '16 at 06:37
  • @CHENGLIANGYE your solution does the finding node, but doesn't explain the update function. Because Update function should go with find filter and update object. My major concern was to update than finding. – HaBo Nov 04 '16 at 08:54
  • @ HaBo you didn't clarify what update you want to do. If you can give some details, I can try to find suitable way to do update. – CHENGLIANG YE Nov 04 '16 at 13:26
0

I have a version, which is based on @DmitryZyr 's answer, and uses 2 answer of the question How do I create an expression tree calling IEnumerable<TSource>.Any(...)?. Thanks to Aaron Heusser and Barry Kelly:

class Program
{
    #region Copied from Expression.Call question
    static MethodBase GetGenericMethod(Type type, string name, Type[] typeArgs, Type[] argTypes, BindingFlags flags)
    {
        int typeArity = typeArgs.Length;
        var methods = type.GetMethods()
            .Where(m => m.Name == name)
            .Where(m => m.GetGenericArguments().Length == typeArity)
            .Select(m => m.MakeGenericMethod(typeArgs));

        return Type.DefaultBinder.SelectMethod(flags, methods.ToArray(), argTypes, null);
    }

    static bool IsIEnumerable(Type type)
    {
        return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>);
    }

    static Type GetIEnumerableImpl(Type type)
    {
        // Get IEnumerable implementation. Either type is IEnumerable<T> for some T, 
        // or it implements IEnumerable<T> for some T. We need to find the interface.
        if (IsIEnumerable(type))
            return type;
        Type[] t = type.FindInterfaces((m, o) => IsIEnumerable(m), null);
        Debug.Assert(t.Length == 1);
        return t[0];
    }

    static Expression CallAny(Expression collection, Expression predicateExpression)
    {
        Type cType = GetIEnumerableImpl(collection.Type);
        collection = Expression.Convert(collection, cType); // (see "NOTE" below)

        Type elemType = cType.GetGenericArguments()[0];
        Type predType = typeof(Func<,>).MakeGenericType(elemType, typeof(bool));

        // Enumerable.Any<T>(IEnumerable<T>, Func<T,bool>)
        MethodInfo anyMethod = (MethodInfo)
            GetGenericMethod(typeof(Enumerable), "Any", new[] { elemType },
                new[] { cType, predType }, BindingFlags.Static);

        return Expression.Call(anyMethod, collection, predicateExpression);
    }
    #endregion

    public class TestEntity
    {
        public int Id { get; set; }
        public TestEntity[] Value { get; set; }
    }

    private static TestEntity CreateTestEntity(int id, params TestEntity[] values)
    {
        return new TestEntity { Id = id, Value = values };
    }

    static void Main(string[] args)
    {
        const string connectionString = "mongodb://localhost:27017";
        var client = new MongoClient(connectionString);

        var db = client.GetDatabase("TestEntities");
        IMongoCollection<TestEntity> collection = db.GetCollection<TestEntity>("Entities");

        collection.InsertOne(CreateTestEntity(1, CreateTestEntity(2, CreateTestEntity(3, CreateTestEntity(4)))));
        const int selectedId = 4;

        int searchDepth = 6;
        // builds the expression tree of expanding x => x.Value.Any(...)
        var filter = GetFilterForDepth(selectedId, searchDepth);
        var testEntity = collection.Find(filter).FirstOrDefault();
        if (testEntity != null)
        {
            UpdateItem(testEntity, selectedId);
            collection.ReplaceOne(filter, testEntity);
        }
    }

    private static bool UpdateItem(TestEntity testEntity, int selectedId)
    {
        if (testEntity.Id == selectedId)
        {
            return true;
        }
        if (UpdateItem(testEntity.Value[0], selectedId))
            testEntity.Value[0] = CreateTestEntity(11);
        return false;
    }

    private static FilterDefinition<TestEntity> GetFilterForDepth(int id, int depth)
    {
        // item
        var idEqualsParam = Expression.Parameter(typeof(TestEntity), "item");
        // .Id
        var idProp = Expression.Property(idEqualsParam, "Id");
        // item.Id == id
        var idEquals = Expression.Equal(idProp, Expression.Constant(id));
        // item => item.Id == id
        var idEqualsLambda = Expression.Lambda<Func<TestEntity, bool>>(idEquals, idEqualsParam);
        // x
        var anyParam = Expression.Parameter(typeof(TestEntity), "x");
        // .Value
        var valueProp = Expression.Property(anyParam, "Value");
        // Expression.Call would not find easily the appropriate .Any((TestEntity x) => x == id)
        // .Value.Any(item => item.Id == id)
        var callAny = CallAny(valueProp, idEqualsLambda);
        // x => x.Value.Any(item => item.Id == id)
        var firstAny = Expression.Lambda<Func<TestEntity, bool>>(callAny, anyParam);

        return NestedFilter(Builders<TestEntity>.Filter.Eq(x => x.Id, id), firstAny, depth);
    }

    static int paramIndex = 0;

    private static FilterDefinition<TestEntity> NestedFilter(FilterDefinition<TestEntity> actual, Expression<Func<TestEntity, bool>> whereExpression, int depth)
    {
        if (depth == 0)
        {
            return actual;
        }
        // paramX
        var param = Expression.Parameter(typeof(TestEntity), "param" + paramIndex++);
        // paramX.Value
        var valueProp = Expression.Property(param, "Value");
        // paramX => paramX.Value.Any(...)
        var callLambda = Expression.Lambda<Func<TestEntity, bool>>(CallAny(valueProp, whereExpression), param);
        return NestedFilter(Builders<TestEntity>.Filter.Where(whereExpression), callLambda, depth - 1) | actual;
    }
}

It's still fixed length depth search, but the depth can be varied dinamically. Just 1 step to upgrade the code to try 1st level, 2nd level, .... And the depth is infinite

Community
  • 1
  • 1
ntohl
  • 2,067
  • 1
  • 28
  • 32