Переглянути джерело

Performance improvements to saving stock movements in bulk

Kenric Nugteren 9 місяців тому
батько
коміт
fd33e11895

+ 1 - 1
prs.stores/EmailStore.cs

@@ -49,7 +49,7 @@ namespace Comal.Stores
             //base.OnSave(entity);
             //base.OnSave(entity);
         }
         }
 
 
-        protected override void OnSave(IEnumerable<Email> entities, ref string auditnote)
+        protected override void OnSave(Email[] entities, ref string auditnote)
         {
         {
             //base.OnSave(entities);
             //base.OnSave(entities);
         }
         }

+ 1 - 1
prs.stores/GPSTrackerLocationStore.cs

@@ -132,7 +132,7 @@ namespace Comal.Stores
             auditnote = null;
             auditnote = null;
         }
         }
 
 
-        protected override void OnSave(IEnumerable<GPSTrackerLocation> entities, ref string auditnote)
+        protected override void OnSave(GPSTrackerLocation[] entities, ref string auditnote)
         {
         {
             var updates = entities.Where(x => !Equals(x.Tracker.ID, Guid.Empty)).ToArray();
             var updates = entities.Where(x => !Equals(x.Tracker.ID, Guid.Empty)).ToArray();
             if (updates.Any())
             if (updates.Any())

+ 1 - 1
prs.stores/ManufacturingHistoryStore.cs

@@ -139,7 +139,7 @@ namespace Comal.Stores
             //base.OnSave(entity);
             //base.OnSave(entity);
         }
         }
 
 
-        protected override void OnSave(IEnumerable<ManufacturingHistory> entities, ref string auditnote)
+        protected override void OnSave(ManufacturingHistory[] entities, ref string auditnote)
         {
         {
             //base.OnSave(entities);
             //base.OnSave(entities);
         }
         }

+ 1 - 1
prs.stores/ModuleTrackingStore.cs

@@ -51,7 +51,7 @@ namespace Comal.Stores
             //base.OnSave(entity);
             //base.OnSave(entity);
         }
         }
 
 
-        protected override void OnSave(IEnumerable<ModuleTracking> entities, ref string auditnote)
+        protected override void OnSave(ModuleTracking[] entities, ref string auditnote)
         {
         {
             //base.OnSave(entities);
             //base.OnSave(entities);
         }
         }

+ 116 - 0
prs.stores/StockHoldingStore.cs

@@ -3,9 +3,12 @@ using InABox.Core;
 using InABox.Database;
 using InABox.Database;
 using System.Linq;
 using System.Linq;
 using System;
 using System;
+using MathNet.Numerics;
 
 
 namespace Comal.Stores;
 namespace Comal.Stores;
 
 
+using HoldingDictionary = Dictionary<(Guid product, Guid style, Guid location, Guid job, StockDimensions dimensions), StockHolding>;
+
 public class StockHoldingStore : BaseStore<StockHolding>
 public class StockHoldingStore : BaseStore<StockHolding>
 {
 {
 
 
@@ -15,6 +18,119 @@ public class StockHoldingStore : BaseStore<StockHolding>
         Decrease
         Decrease
     }
     }
 
 
+    public static StockMovement[] LoadMovementData(IStore store, Guid[] ids)
+    {
+        return store.Provider.Query(
+            new Filter<StockMovement>(x => x.ID).InList(ids),
+            Columns.None<StockMovement>().Add(x => x.ID)
+                .Add(x => x.Location.ID)
+                .Add(x => x.Product.ID)
+                .Add(x => x.Style.ID)
+                .Add(x => x.Job.ID)
+                .Add(x => x.Dimensions.Unit.ID)
+                .Add(x => x.Dimensions.Quantity)
+                .Add(x => x.Dimensions.Height)
+                .Add(x => x.Dimensions.Width)
+                .Add(x => x.Dimensions.Length)
+                .Add(x => x.Dimensions.Weight)
+                .Add(x => x.Dimensions.UnitSize)
+                .Add(x => x.Dimensions.Value)
+                .Add(x => x.JobRequisitionItem.ID)
+                .Add(x => x.Units)
+                .Add(x => x.Cost)
+        ).ToArray<StockMovement>();
+    }
+
+    public static HoldingDictionary LoadStockHoldings(IStore store, StockMovement[] mvts, HoldingDictionary? holdings = null)
+    {
+        if(holdings is not null)
+        {
+            mvts = mvts.Where(mvt =>
+            {
+                var key = (mvt.Product.ID, mvt.Style.ID, mvt.Location.ID, mvt.Job.ID, mvt.Dimensions);
+                return !holdings.ContainsKey(key);
+            }).ToArray();
+        }
+        else
+        {
+            holdings = new();
+        }
+
+        var productIDs = mvts.Select(x => x.Product.ID).Distinct().ToArray();
+        var locationIDs = mvts.Select(x => x.Location.ID).Distinct().ToArray();
+        var styleIDs = mvts.Select(x => x.Style.ID).Distinct().ToArray();
+        var jobIDs = mvts.Select(x => x.Job.ID).Distinct().ToArray();
+
+        var newHoldings = store.Provider.Query(new Filter<StockHolding>(x => x.Product.ID).InList(productIDs)
+                .And(x => x.Location.ID).InList(locationIDs)
+                .And(x => x.Style.ID).InList(styleIDs)
+                .And(x => x.Job.ID).InList(jobIDs),
+                Columns.None<StockHolding>().Add(x => x.ID)
+                    .Add(x => x.Units)
+                    .Add(x => x.Qty)
+                    .Add(x => x.Value)
+                    .Add(x => x.Available)
+                    .Add(x => x.Weight)
+                    .Add(x => x.AverageValue)
+                    .Add(x => x.Product.ID)
+                    .Add(x => x.Location.ID)
+                    .Add(x => x.Style.ID)
+                    .Add(x => x.Job.ID)
+                    .AddDimensionsColumns(x => x.Dimensions, Dimensions.ColumnsType.Local)
+            ).ToObjects<StockHolding>();
+        foreach(var holding in newHoldings)
+        {
+            holdings[(holding.Product.ID, holding.Style.ID, holding.Location.ID, holding.Job.ID, holding.Dimensions)] = holding;
+        }
+
+        return holdings;
+    }
+
+    public static void ModifyHoldings(StockMovement[] mvts, HoldingDictionary holdings, Action action)
+    {
+        foreach(var mvt in mvts)
+        {
+            var key = (mvt.Product.ID, mvt.Style.ID, mvt.Location.ID, mvt.Job.ID, mvt.Dimensions);
+            var holding = holdings.GetValueOrDefault(key);
+            if(holding is null)
+            {
+                holding = new();
+                holding.Location.ID = mvt.Location.ID;
+                holding.Product.ID = mvt.Product.ID;
+                holding.Style.ID = mvt.Style.ID;
+                holding.Job.ID = mvt.Job.ID;
+                holding.Dimensions.CopyFrom(mvt.Dimensions);
+                holdings[key] = holding;
+            }
+
+            double multiplier = action == Action.Increase ? 1F : -1F;
+            holding.Units += (multiplier * mvt.Units);
+            holding.Qty += (multiplier * mvt.Units * mvt.Dimensions.Value);
+            holding.Value += (multiplier * mvt.Units * mvt.Cost);
+            holding.Available += (multiplier * (mvt.JobRequisitionItem.ID == Guid.Empty ? mvt.Units : 0.0));
+            
+            holding.Weight = holding.Qty * holding.Dimensions.Weight;
+            holding.AverageValue = holding.Units != 0 ? holding.Value / holding.Units : 0.0F;
+        }
+    }
+
+    public static void SaveHoldings(IStore store, HoldingDictionary holdings)
+    {
+        var holdingStore = store.FindSubStore<StockHolding>();
+        holdingStore.Delete(
+            holdings.Values.Where(x => x.ID != Guid.Empty && x.Units.IsEffectivelyEqual(0.0) && x.Available.IsEffectivelyEqual(0.0)), "");
+        holdingStore.Save(
+            holdings.Values.Where(x => x.IsChanged() && (!x.Units.IsEffectivelyEqual(0.0) || !x.Available.IsEffectivelyEqual(0.0))), "");
+    }
+
+    public static void UpdateStockHoldings(IStore store, Guid[] ids, Action action)
+    {
+        var movements = LoadMovementData(store, ids);
+        var holdings = LoadStockHoldings(store, movements);
+        ModifyHoldings(movements, holdings, action);
+        SaveHoldings(store, holdings);
+    }
+
     /// <summary>
     /// <summary>
     /// Maintains the Stock Holding Table when manipulating Stock Movements
     /// Maintains the Stock Holding Table when manipulating Stock Movements
     /// We only accept an ID, because the rest of the movement is pulled from the database
     /// We only accept an ID, because the rest of the movement is pulled from the database

+ 74 - 42
prs.stores/StockMovementStore.cs

@@ -9,60 +9,92 @@ using InABox.Database;
 
 
 namespace PRSStores;
 namespace PRSStores;
 
 
+using HoldingDictionary = Dictionary<(Guid product, Guid style, Guid location, Guid job, StockDimensions dimensions), StockHolding>;
+
 public class StockMovementStore : BaseStore<StockMovement>
 public class StockMovementStore : BaseStore<StockMovement>
 {
 {
-    protected override void BeforeSave(StockMovement sm)
-    {
-        base.BeforeSave(sm);
-        
-        // If this movement is an Update (instead of Insert),
-        // we need to reduce the old stock holding before updating the new one
-        if (sm.ID != Guid.Empty)
-            StockHoldingStore.UpdateStockHolding(this, sm.ID,StockHoldingStore.Action.Decrease);
+    // These will be initialised in BeforeSave
+    HoldingDictionary holdingData = null!;
+    StockMovement[] mvtData = null!;
 
 
-        if(sm.Job.HasOriginalValue(x => x.ID) && sm.Job.ID != Guid.Empty && sm.JobScope.ID == Guid.Empty)
+    protected override void BeforeSave(IEnumerable<StockMovement> entities)
+    {
+        foreach(var entity in entities)
         {
         {
-            // If we have updated the Job.ID to a non-empty value, we should
-            // update the JobScope to the default job scope for that job.
+            // Calling base BeforeSave so that it doesn't call our other BeforeSave method.
+            base.BeforeSave(entity);
+        }
 
 
-            var scopeID = Guid.Empty;
-            if(sm.ID != Guid.Empty)
-            {
-                // It's possible that the JobScope ID just wasn't passed up, if
-                // this entity already exists; hence, we shall load the scopeID
-                // from the database just in case.
-                scopeID = Provider.Query(
-                    new Filter<StockMovement>(x => x.ID).IsEqualTo(sm.ID),
-                    Columns.None<StockMovement>().Add(x => x.JobScope.ID))
-                    .Rows.FirstOrDefault()?.Get<StockMovement, Guid>(x => x.JobScope.ID) ?? Guid.Empty;
-            }
-            if(scopeID == Guid.Empty)
+        mvtData = StockHoldingStore.LoadMovementData(this, entities.Select(x => x.ID).ToArray());
+        holdingData = StockHoldingStore.LoadStockHoldings(this, mvtData);
+
+        StockHoldingStore.ModifyHoldings(mvtData, holdingData, StockHoldingStore.Action.Decrease);
+
+        foreach(var sm in entities)
+        {
+            if(sm.Job.HasOriginalValue(x => x.ID) && sm.Job.ID != Guid.Empty && sm.JobScope.ID == Guid.Empty)
             {
             {
-                // No scope has been assigned; however, we have a job, so we
-                // load the default scope for the job.
-                sm.JobScope.ID = Provider.Query(
-                    new Filter<Job>(x => x.ID).IsEqualTo(sm.Job.ID),
-                    Columns.None<Job>().Add(x => x.DefaultScope.ID))
-                    .Rows.FirstOrDefault()?.Get<Job, Guid>(x => x.DefaultScope.ID) ?? Guid.Empty;
+                // If we have updated the Job.ID to a non-empty value, we should
+                // update the JobScope to the default job scope for that job.
+
+                var scopeID = Guid.Empty;
+                if(sm.ID != Guid.Empty)
+                {
+                    // It's possible that the JobScope ID just wasn't passed up, if
+                    // this entity already exists; hence, we shall load the scopeID
+                    // from the database just in case.
+                    scopeID = Provider.Query(
+                        new Filter<StockMovement>(x => x.ID).IsEqualTo(sm.ID),
+                        Columns.None<StockMovement>().Add(x => x.JobScope.ID))
+                        .Rows.FirstOrDefault()?.Get<StockMovement, Guid>(x => x.JobScope.ID) ?? Guid.Empty;
+                }
+                if(scopeID == Guid.Empty)
+                {
+                    // No scope has been assigned; however, we have a job, so we
+                    // load the default scope for the job.
+                    sm.JobScope.ID = Provider.Query(
+                        new Filter<Job>(x => x.ID).IsEqualTo(sm.Job.ID),
+                        Columns.None<Job>().Add(x => x.DefaultScope.ID))
+                        .Rows.FirstOrDefault()?.Get<Job, Guid>(x => x.DefaultScope.ID) ?? Guid.Empty;
+                }
             }
             }
         }
         }
     }
     }
 
 
-    protected override void AfterSave(StockMovement sm)
+    protected override void BeforeSave(StockMovement sm)
+    {
+        BeforeSave(CoreUtils.One(sm));
+    }
+
+    protected override void AfterSave(IEnumerable<StockMovement> entities)
     {
     {
         // Update the Relevant StockHolding with the details of this movement
         // Update the Relevant StockHolding with the details of this movement
-        StockHoldingStore.UpdateStockHolding(this, sm.ID,StockHoldingStore.Action.Increase);
-        
-        // Update the Job requisition item status (if applicable)
-        if (sm.JobRequisitionItem.ID != Guid.Empty)
-            JobRequisitionItemStore.UpdateStatus(
-                this, 
-                sm.JobRequisitionItem.ID, 
-                sm.HasOriginalValue(x=>x.ID) 
-                    ? JobRequisitionItemAction.Created 
-                    : JobRequisitionItemAction.Updated
-            );
-        base.AfterSave(sm);
+        StockHoldingStore.ModifyHoldings(mvtData, holdingData, StockHoldingStore.Action.Increase);
+        StockHoldingStore.SaveHoldings(this, holdingData);
+
+        foreach(var sm in entities)
+        {
+            // Update the Job requisition item status (if applicable)
+            if (sm.JobRequisitionItem.ID != Guid.Empty)
+                JobRequisitionItemStore.UpdateStatus(
+                    this, 
+                    sm.JobRequisitionItem.ID, 
+                    sm.HasOriginalValue(x=>x.ID) 
+                        ? JobRequisitionItemAction.Created 
+                        : JobRequisitionItemAction.Updated
+                );
+        }
+
+        foreach(var entity in entities)
+        {
+            // Calling base AfterSave so that it doesn't call our other AfterSave method.
+            base.AfterSave(entity);
+        }
+    }
+
+    protected override void AfterSave(StockMovement sm)
+    {
+        AfterSave(CoreUtils.One(sm));
     }
     }
 
 
     protected override void BeforeDelete(StockMovement entity)
     protected override void BeforeDelete(StockMovement entity)