DataScope tracking deleted entities from collection

Posts   
 
    
yowl
User
Posts: 271
Joined: 11-Feb-2008
# Posted on: 30-Jun-2021 22:46:26   

Hi,

I'm adding a collection to a DataScope with the Attach method, and I expected this collection to be tracked, but it is not, only the collections for child properties seen in entities in the root collection are tracked. Is this by design? Looking at the code I can see that maybe I could replicate the relevant part of HandleOnAdd in DataScopeContext using reflection, but what would you recommend? I don't think I've done anything wrong as HandleOnAdd seems to be the only place where collections get tracked and that is not appropriate for the top level collection, but of course I may well be missing something.

LLBLGen 5.8 Adapter.

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39760
Joined: 17-Aug-2003
# Posted on: 01-Jul-2021 09:18:17   

This is indeed by design, as the use case is typically one where the entities have been fetched using an entity collection and then added in bulk using that method, but you don't want the container (the entity collection) to be tracked as that's just a container that's discarded. The entity collection containing the entities either is part of an entity (which then should be tracked) or a container that's not used anymore after the fetch has been completed.

(edit) Actually looking at the design docs shows this isn't entirely correct: the entity collection should be added to the context as well when passed to DataScope.Attach(). However When you are using TrackQuery, the collection container the result is returned in isn't added.

You store the collection somewhere?

Frans Bouma | Lead developer LLBLGen Pro
yowl
User
Posts: 271
Joined: 11-Feb-2008
# Posted on: 01-Jul-2021 14:15:08   

Yes the collection is retrieved from the database, and then displayed in a grid in the UI. The user can then add/insert/delete rows in the grid. This is a typical scenario for me, especially with reference data screens, like maintaining companies or customers. In general, in my experience, transactional screens either have a root element (which is catered for with DataScope), or they have a root collection, like in this scenario.

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39760
Joined: 17-Aug-2003
# Posted on: 02-Jul-2021 09:15:46   

Alright. We can add a flag to the attach method for collections (so basically add an overload) which by default is false but when set to true also tracks the collection specified. It shouldn't be a big change, so we'll squeeze it in for 5.9

Frans Bouma | Lead developer LLBLGen Pro
yowl
User
Posts: 271
Joined: 11-Feb-2008
# Posted on: 02-Jul-2021 14:31:13   

That would be perfect. Thanks!

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39760
Joined: 17-Aug-2003
# Posted on: 14-Jul-2021 11:27:36   

I've looked back at the design documents of this feature but what I mentioned above isn't entirely correct: when passing an entity collection to the DataScope.Attach() method, it is added to the internal context as well as its contents, what I mentioned above is true for when you call TrackQuery: the resulting entity collection returned by a query isn't tracked.

We setup a test to try to reproduce what you mentioned, but we can't, it works as you expected:

[Test]
public void InsertThroughCollectionTest()
{
    var scope = new UpdateInsertDataScope(10254);
    var orderDetails = new EntityCollection<OrderDetailsEntity>();
    scope.Add(orderDetails);
    
    // add new entity to collection, it should be picked up now 
    var newOrderDetail = new OrderDetailsEntity();
    newOrderDetail.ProductId = 1;
    newOrderDetail.OrderId = 10254;
    newOrderDetail.Quantity = (short)10;
    newOrderDetail.Discount = 0.0f;
    newOrderDetail.UnitPrice = 10.2M;
    orderDetails.Add(newOrderDetail);
    
    UnitOfWork2 uow = null;
    Func<IUnitOfWorkCore, bool> commitFunc = a => { uow = (UnitOfWork2)a; return true; };
    scope.CommitChanges(commitFunc);
    uow.ConstructSaveProcessQueues();
    var insertQueue = uow.GetInsertQueue();
    var updateQueue = uow.GetUpdateQueue();
    Assert.AreEqual(1, insertQueue.Count);
    Assert.AreEqual(0, updateQueue.Count);
    Assert.AreEqual(newOrderDetail, insertQueue.First().Entity);
}

// the Add method in the DataScope derived class:
public void Add(IEntityCollection2 entityCollection)
{
    this.Attach(entityCollection);
}

This test succeeds correctly, as expected. We add an empty collection to the scope, then add an entity to the collection and it's picked up by the scope as expected.

Attaching a filled collection also works:

[Test]
public void UpdateThroughCollectionTest()
{
    var scope = new UpdateDataScope("USA");
    Assert.IsTrue(scope.FetchData());
    var customers = scope.Customers;
    customers[0].City += "Foo";

    UnitOfWork2 uow = null;
    Func<IUnitOfWorkCore, bool> commitFunc = a => { uow = (UnitOfWork2)a; return true; };
    scope.CommitChanges(commitFunc);
    uow.ConstructSaveProcessQueues();
    var insertQueue = uow.GetInsertQueue();
    var updateQueue = uow.GetUpdateQueue();
    Assert.AreEqual(0, insertQueue.Count);
    Assert.AreEqual(1, updateQueue.Count);
    Assert.AreEqual(customers[0], updateQueue.First().Entity);
}

// scope:
public class UpdateDataScope : DataScope
{
    #region Class Member Declarations
    private string _country;
    private EntityCollection<CustomerEntity> _customers;
    #endregion

    public UpdateDataScope(string country)
    {
        _country = country;
    }

    protected override bool FetchDataImpl(params object[] fetchMethodParameters)
    {
        using(var adapter = new DataAccessAdapter())
        {
            _customers = new EntityCollection<CustomerEntity>();
            adapter.FetchEntityCollection(_customers, new RelationPredicateBucket(CustomerFields.Country==_country));
            this.Attach(_customers);
        }
        return _customers.Count>0;
    }

    public void Add(IEntityCollection2 entityCollection)
    {
        this.Attach(entityCollection);
    }

    
    public EntityCollection<CustomerEntity> Customers
    {
        get { return _customers; }
    }
}

Test succeeds as well. Now, if you use the TrackQuery to attach that entitycollection, then it's by design that it isn't added. However if you're using the above construct, then we're looking at a bug to fix instead.

Could you give us more information what exactly you're doing in this particular case so we can try to replicate it here and see what needs changing?

Frans Bouma | Lead developer LLBLGen Pro
yowl
User
Posts: 271
Joined: 11-Feb-2008
# Posted on: 14-Jul-2021 14:48:55   

Thanks for the test code. It's the delete that is the problem. The collection is fetched and bound to grid. The user deletes a row from the grid and that is not tracked. The datascope is persisted but the delete does not occur. I've updated the test with a delete operation which fails. (I also changed the entity name to match mine, so you might want to put that back to CustomerEntity and update the properties)

        [Test]
        public void UpdateThroughCollectionTest()
        {
            var scope = new UpdateDataScope("USA");
            Assert.IsTrue(scope.FetchData());
            var customers = scope.Customers;
            customers[0].AccountNumber += "Foo";
            customers.RemoveAt(1);

            UnitOfWork2 uow = null;
            Func<IUnitOfWorkCore, bool> commitFunc = a =>
            {
                uow = (UnitOfWork2) a;
                return true;
            };
            scope.CommitChanges(commitFunc);
            uow.ConstructSaveProcessQueues();
            var insertQueue = uow.GetInsertQueue();
            var updateQueue = uow.GetUpdateQueue();
            var toDelete = uow.GetEntityElementsToDelete();
            Assert.AreEqual(0, insertQueue.Count);
            Assert.AreEqual(1, updateQueue.Count);
            Assert.AreEqual(1, toDelete.Count);
            Assert.AreEqual(customers[0], updateQueue.First().Entity);
        }

        // scope:
        public class UpdateDataScope : DataScope
        {
            #region Class Member Declarations

            private string _country;
            private EntityCollection<SalesOrderHeaderEntity> _customers;

            #endregion

            public UpdateDataScope(string country)
            {
                _country = country;
            }

            protected override bool FetchDataImpl(params object[] fetchMethodParameters)
            {
                _customers = new EntityCollection<SalesOrderHeaderEntity>();
                _customers.Add(new SalesOrderHeaderEntity
                {
                    IsNew = false,
                    SalesOrderId = 1,
                });
                _customers.Add(new SalesOrderHeaderEntity
                {
                    IsNew = false,
                    SalesOrderId = 2,
                });
                _customers[0].IsDirty = false;
                _customers[1].IsDirty = false;
                this.Attach(_customers);

                return _customers.Count > 0;
            }

            public void Add(IEntityCollection2 entityCollection)
            {
                this.Attach(entityCollection);
            }


            public EntityCollection<SalesOrderHeaderEntity> Customers
            {
                get { return _customers; }
            }
        }
 
Otis avatar
Otis
LLBLGen Pro Team
Posts: 39760
Joined: 17-Aug-2003
# Posted on: 14-Jul-2021 20:29:41   

Thanks, we'll have a look (I think the problem is mainly that there's no removal tracker collection inserted in the attached collection, which is actually a bug).

Frans Bouma | Lead developer LLBLGen Pro
Otis avatar
Otis
LLBLGen Pro Team
Posts: 39760
Joined: 17-Aug-2003
# Posted on: 15-Jul-2021 13:39:20   

We've fixed it, please download the latest 5.8.2 hotfix to fix this problem.

Frans Bouma | Lead developer LLBLGen Pro
yowl
User
Posts: 271
Joined: 11-Feb-2008
# Posted on: 15-Jul-2021 18:38:30   

Thanks very much.