mbdavid / LiteDB

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

[QUESTION] Proper way of CRUD operations on nested collections with DBRef #1847

Open Eagle2 opened 3 years ago

Eagle2 commented 3 years ago

Hello, I struggle with creating proper CRUD operations on nested collections with DBRef.

My questions are:

  1. Should I use DBRef when at least one level of nesting occured?

2a. How to make proper CRUD operations on nested collection when using name resolver and DBRef on this collection? I mean, what can I upgrade and is my approach correct ?

For now in ViewModel I do mappings from app objects to POCOs (with AutoMapper) and then I have to make 2 calls to database - first, to add ID (DBRef) of new child object and latter is to insert child object to its own collection.

2b. Is it possible to update child collection, using .Update() on parent only ?

My code goes like this (i stripped some code, can provide more if needed):

ViewModel method which adds new CodeCheckRule (child) to RulesSet (parent):

        public void AddCodeCheckRule()
        {
            try
            {
...
                if ((bool)result)
                {
                    // Set ParentSetId
                    viewModel.VMObject.ParentSetId = SelectedRulesSet.Id;

                    // Adding nested object to it's own collection
                    var dtoCodeCheckRule = ObjectMapper.Mapper.Map<CodeCheckRuleViewModel, CodeCheckRule>(viewModel.VMObject);
                    CodeCheckRulesCollection.Collection.Insert(dtoCodeCheckRule);

                    // Storing reference to new object in nested collection
                    var setDTO = ObjectMapper.Mapper.Map<BindableRulesSet, RulesSet>(SelectedRulesSet);
                    setDTO.Rules.Add(dtoCodeCheckRule);
                    RulesSetsCollection.Collection.Update(setDTO.Id, setDTO);

                    CodeCheckRules.Clear();
                    var toConvert = CodeCheckRulesCollection.Collection.Find(x => x.ParentSetId == SelectedRulesSet.Id);
                    foreach (var rule in toConvert)
                    {
                        CodeCheckRules.Add(ObjectMapper.Mapper.Map<CodeCheckRule, BindableCodeCheckRule>(rule));
                    }
                }
            }
            catch (Exception ex)
            {
               ...
            }
        }

My Collection Name resolver and mapper:

...
            BsonMapper.Global.Entity<RulesSet>()
                .Id(x => x.Id)
                .DbRef(x => x.Rules, codeCheckRulesCollectionName);

            BsonMapper.Global.Entity<CodeCheckRule>()
                .Id(x=>x.Id)
                .DbRef(x => x.Qualifiers, operationDefinitionsCollectionName)
                .DbRef(x => x.Analyzers, operationDefinitionsCollectionName)
                .DbRef(x => x.Manipulators, operationDefinitionsCollectionName);
...

            BsonMapper.Global.ResolveCollectionName = (x) =>
            {
                switch (x)
                {
                    case Type type when type == typeof(RobotStandard):
                        return robotStandardCollectionName;

                    case Type type when type == typeof(RulesSet):
                        return rulesSetCollectionName;

                    case Type type when type == typeof(CodeCheckRule):
                        return codeCheckRulesCollectionName;

                    case Type type when type == typeof(RobotModel):
                        return robotModelsCollectionName;

                    case Type type when type == typeof(TechPacket):
                        return requiredTechPacketsCollectionName;

                    case Type type when type == typeof(OperationDefinition):
                        return operationDefinitionsCollectionName;

                    case Type type when type == typeof(RobotDefaults):
                        return robotDefaultsCollectionName;
                    default:
                        // return mulitple form of collection name - with "s" suffix
                        return x.Name + "s";
                }
            };

Parent Class (I show just Guid and collections, no other properties)

    public class BindableRulesSet : EditableViewModelObject
    {
 ...
        public Guid Id { get; set; } = Guid.NewGuid();

        public BindableCollection<BindableLocalizedString> Names { get; set; } = new BindableCollection<BindableLocalizedString>();

        public BindableCollection<BindableLocalizedString> Descriptions { get; set; } = new BindableCollection<BindableLocalizedString>();

        public BindableCollection<BindableCodeCheckRule> Rules { get; set; } = new BindableCollection<BindableCodeCheckRule>();
  ...
    }

Child class which has DBRef on parent class

 public class BindableCodeCheckRule : EditableViewModelObject
    { 
...
        public Guid Id { get; set; } = Guid.NewGuid();

        public Guid ParentSetId { get; set; }

        public BindableCollection<BindableLocalizedString> Names { get; set; } = new BindableCollection<BindableLocalizedString>();

        public BindableCollection<BindableLocalizedString> Descriptions { get; set; } = new BindableCollection<BindableLocalizedString>();

        public BindableCollection<ICodeOperation> Qualifiers { get; set; } = new BindableCollection<ICodeOperation>();
        public BindableCollection<ICodeOperation> Analyzers { get; set; } = new BindableCollection<ICodeOperation>();
        public BindableCollection<ICodeOperation> Manipulators { get; set; } = new BindableCollection<ICodeOperation>();
...

Looking forward for advices :)

AlBannaTechno commented 3 years ago

@Eagle2

Should I use DBRef when at least one level of nesting occurred? No, once you have shared items, you should use DBRef, but if you just have a collection/list of items that are tightly coupled to a specific type, eg phone numbers of a user, there is no need to have a separate collection,

just use a list, but there is a trick, if prefer to create an id for a nested object, just remember liteDb will not automatically assign a new id for nested objects it is true, by logic

so

        class Phone
        {
            public Guid Id { get; set; }
            public string Value { get; set; }
            public string Title { get; set; }
        }

        class MyClass
        {
            public string Name { get; set; }
            public List<Phone> Phones { get; set; }
        }

then when inserting a new phone number, just create a new id

      var phone = new Phone {Id = Guid.NewGuid()};

and do not set id generation for GUID in a constructor, it just cost you extra operations when querying items from the database

2a

Please see this https://github.com/mbdavid/LiteDB/issues/1897

2b it possible to update child collection, using.Update() on parent only ?

Yes, there is a thing you must know, LiteDB Engine only look for the id of referenced entity, and noting else

this means if you have

    class Phone
        {
            public Guid Id { get; set; }
            public string Value { get; set; }
            public string Title { get; set; }

        }

        public class PhoneReferencer
        {
            public Guid Id { get; set; }
            public string Value { get; set; } // have the same name and type of Phone.Value 
        }

        class User
        {
            public string Name { get; set; }
            [BsonRef("Phones")]
            public List<PhoneReferencer> Phones { get; set; }
        }

the query will works, Notice we used PhoneReferencer type to reference Phones collection, but this collection is of type Phone not PhoneReferencer, but because both have GUID Id the logic will work for all CRUD operations, you can use that to prevent query all properties of Phone when using .Include(u => u.Phones)

I hope this helps you.