mbdavid / LiteDB

LiteDB - A .NET NoSQL Document Store in a single data file
http://www.litedb.org
MIT License
8.61k stars 1.25k forks source link

LazyLoading Issue #1608

Open rfidstammtisch opened 4 years ago

rfidstammtisch commented 4 years ago

I´ve a question why can´t I use a method from my Class in an Expression? Thats my example code:

namespace LiteDbTests
{
    public class Test
    {
        public string Value { get => "Test"; }

        public bool Test1() => true;
    }

    class Program
    {
        static void Main(string[] args)
        {
            Test test;
            using (var dbLite = new LiteDatabase($"Filename=D:\\Test.liteDb"))
            {
                // for global resources sessionId will be string.Empty
                var collection = dbLite.GetCollection<Test>();
                test = collection.FindOne(i => i.Test1());
            }

            Console.WriteLine(test?.Value ?? "No Test");

            test = new Test();
            using (var dbLite = new LiteDatabase($"Filename=D:\\Test.liteDb"))
            {
                // for global resources sessionId will be string.Empty
                var collection = dbLite.GetCollection<Test>();
                collection.Insert(test);
            }

            using (var dbLite = new LiteDatabase($"Filename=D:\\Test.liteDb"))
            {
                // for global resources sessionId will be string.Empty
                var collection = dbLite.GetCollection<Test>();
                test = collection.FindOne(i => i.Test1());
            }

            Console.WriteLine(test?.Value ?? "No Test");
        }
    }
}

The problem we have is, our object which we wanne store in LiteDb has some kind of lazyloading to restore all members which are not saved in the Db and we want to access this members in the expression to find an specific object.

lbnascimento commented 4 years ago

@rfidstammtisch Your example doesn't work because LiteDB converts your Linq expression to a BsonExpression that is applied to each document when the query is executed. This only works for "known" expressions, like mystring.ToUpper() or Math.Min(num1, num2). When the Linq-to-BsonExpression converter finds an unknown expression, it throws. It is important to note that a BsonExpression is applied to documents before they are mapped to a class, so it is not possible to use expressions that only make sense after mapping (like a instance method).

LiteDB v4 worked around this issue by trying to convert the Linq expression to a BsonExpression and, if the conversion failed, it stored the expression and executed it after the query using Linq-to-Objects, which was very inefficient, because it required deserializing and mapping all the documents in the collection (possibly millions) before applying the filter expression. This feature was removed in v5 due to this performance issue.

If you provided more info about what you're trying to do, maybe I could help you work around this limitation.

rfidstammtisch commented 4 years ago

I´ve written a test project which explainse the problem so, its a bit havier than the one before but here it is:

using LiteDB;
using System;
using System.Collections.Generic;

namespace LiteDbTests
{
    #region Depth

    public class ThirdDepth : ISaveable<ThirdDepth>
    {
        private Func<string, FirstDepth> getFirstDepth;

        // it´s only an example so think about the getFirstDepth-Method as a method which loads a class from a RDB
        // which is too big to save it into LiteDb and it´s not suppost to be in there
        public FirstDepth Circular { get => getFirstDepth(Name); }
        public string Name { get; set; }
        public object Value { get; set; }

        public ThirdDepth(Func<string, FirstDepth> getFirstDepth)
        {
            this.getFirstDepth = getFirstDepth;
        }

        // default ctor for LiteDb
        public ThirdDepth() { }

        public ThirdDepth Restore(Saveable<ThirdDepth> saveable)
        {
            return new ThirdDepth
            {
                Name = saveable["Name"]?.ToString(),
                Value = saveable["Value"],
            };
        }

        public Saveable<ThirdDepth> ToSaveable()
        {
            return new Saveable<ThirdDepth>(new Dictionary<string, object>
            {
                { "Name", this.Name },
                { "Value", this.Value },
            });
        }
    }

    public class SecondDepth : ISaveable<SecondDepth>
    {
        public ThirdDepth ThirdDepth { get; set; }
        public string Key { get; set; }
        public string Version { get; set; }

        public SecondDepth Restore(Saveable<SecondDepth> saveable)
        {
            return new SecondDepth
            {
                Key = saveable["Key"]?.ToString(),
                Version = saveable["Version"]?.ToString(),
                ThirdDepth = (saveable["ThirdDepth"] as Saveable<ThirdDepth>)?.Restore()
            };
        }

        public Saveable<SecondDepth> ToSaveable()
        {
            return new Saveable<SecondDepth>(new Dictionary<string, object>
            {
                { "Key", this.Key },
                { "Version", this.Version},
                { "ThirdDepth", this.ThirdDepth.ToSaveable() },
            });
        }
    }

    public class FirstDepth : ISaveable<FirstDepth>
    {
        public string Foo { get; set; }
        public int Count { get; set; }
        public List<SecondDepth> SecondDepths { get; set; }
        public SecondDepth FirstSecondDepth { get => SecondDepths != null && SecondDepths.Count > 0 ? SecondDepths[0] : null; }

        public FirstDepth Restore(Saveable<FirstDepth> saveable)
        {
            var firstDepth = new FirstDepth
            {
                Count = int.Parse(saveable["Count"].ToString()),
                Foo = saveable["Foo"].ToString(),
                SecondDepths = new List<SecondDepth>(),
            };

            foreach (var secondDepth in saveable["SecondDepths"] as List<Saveable<SecondDepth>>)
            {
                var s = secondDepth.Restore();
                s.ThirdDepth = new ThirdDepth((name) => firstDepth)
                {
                    Name = s.ThirdDepth.Name,
                    Value = s.ThirdDepth.Name
                };

                firstDepth.SecondDepths.Add(s);
            }

            return firstDepth;
        }

        public Saveable<FirstDepth> ToSaveable()
        {
            var saveable = new Saveable<FirstDepth>(new Dictionary<string, object>
            {
                { "Foo", this.Foo },
                { "Count", this.Count },
                { "SecondDepths", new List<Saveable<SecondDepth>>() }
            });

            foreach (var secondDepth in this.SecondDepths)
                (saveable["SecondDepths"] as List<Saveable<SecondDepth>>)?.Add(secondDepth.ToSaveable());

            return saveable;
        }
    }

    #endregion

    #region Saveable

    public interface ISaveable<T>
        where T : ISaveable<T>, new()
    {
        Saveable<T> ToSaveable();
        T Restore(Saveable<T> saveable);
    }

    public class Saveable<T>
        where T : ISaveable<T>, new()
    {
        public Saveable(Dictionary<string, object> saveable)
        {
            SaveableObject = saveable;
        }

        // here we tried a Dynamic object wat were not able to restore it from the Database
        public Dictionary<string, object> SaveableObject { get; private set; }
        public T Restore() => new T().Restore(this);

        public object this[string key]
        {
            get
            {
                return SaveableObject[key];
            }
            set
            {
                SaveableObject[key] = value;
            }
        }
    }

    #endregion

    #region Program

    class Program
    {
        static void Main(string[] args)
        {
            FirstDepth test = null;

            test = new FirstDepth
            {
                Foo = "Foo",
                Count = 1,
                SecondDepths = new List<SecondDepth>(),
            };

            test.SecondDepths.Add(new SecondDepth
            {
                Key = "Some",
                Version = "1",
                ThirdDepth = new ThirdDepth((name) => test)
                {
                    Name = "Name",
                    Value = new { Test = "Thats not working in Version 4" }
                }
            });

            using (var dbLite = new LiteDatabase($"Filename=D:\\Test.liteDb"))
            {
                // for global resources sessionId will be string.Empty
                var collection = dbLite.GetCollection<Saveable<FirstDepth>>("Saveable");
                collection.Insert(test.ToSaveable());
            }

            var foo = test.ToSaveable().Restore();

            using (var dbLite = new LiteDatabase($"Filename=D:\\Test.liteDb"))
            {
                // for global resources sessionId will be string.Empty
                var collection = dbLite.GetCollection<Saveable<FirstDepth>>("Saveable");
                var saveable = collection.FindOne(s => s.Restore().Count > 0 && s.Restore().SecondDepths[0].ThirdDepth.Circular.Foo == "Test");

                if (saveable != null)
                    test = saveable.Restore();
            }

            Console.WriteLine($"ThirdDepth Circulate {test?.FirstSecondDepth?.ThirdDepth.Circular?.Foo}");
        }
    }

    #endregion
}
lbnascimento commented 4 years ago

@rfidstammtisch Sorry about the delay. I don't think it is currently possible to use your classes with our BsonMapper without some major changes to them. Our mapper is only meant to handle POCO classes, even though it can handle simple cases of inheritance. If you were willing to make major changes to your classes, I could help you if you informed gave some more information on what you're trying to achieve.

Also, another alternative would be to create your own standalone mapping methods, that converted from and to BsonDocument.

rfidstammtisch commented 4 years ago

@lbnascimento we can rearchitect the ISaveable interface for sure but the Depth hierarchy unfortunately not. (so to say not that easy) ... if you can provide an idea to save an complex object and to restore it. Please help me.

What we want to archive is, to save many complex objects with unknown inheritance depth and unknown member count.

Another idea was to write POCO's for all classes and save them. But there we still have the restore and the expression problem.

What we trying to archival is: save some config files for our application as simple as it is, we done it with SQLite and now wanted to migrate to LiteDb.

I think its possibile to do so but we did not get the right idea by now.