Working with object change history

Object change history is often used in reporting to get the data about property changes. This include changes such as Comments, workflow states, and other key properties where changes to the properties requires monitoring.

The following procedures are related:
  • spMFUpdateObjectChangeHistory

  • spMFupdateAllncludedInAppTables

  • spMFUpdateMfilesToMFSQL

  • spMFGetHistory

The following tables are related:
  • MFObjectChangeHistory

  • MFObjectChangeHistoryUpdateControl

Updating of the object change history is included in spmfUpdateAllIncludedInAppTables and spmfUpdateMFilestoMFSQL

After taking the steps outlined below spmfUpdateMFilestoMFSQL will automatically include processing spMFUpdateObjectChangeHistory. spMFUpdateobjectChangeHistory uses the MFObjectChangeHistoryUpdateControl table to get all the classes and properties and then use spMFGetHistory to perform the updates.

spMFUpdateobjectChangeHistory can be executed on its own using the parameters to set the scope. See below for more detail.

  • spMFGetHistory will be processed for each row in the control table (consisting of a valid class table name and a valid property column name)

  • All records in the class table with values for the specific property will be included

  • The start date of the update will be set to the last change date in for the specific class and property in the MFObjectChangeHistory table.

  • The procedure will therefor only update new changes for the property.

MFObjectChangeHistory maintains a cummulative object change history for the designated properties.

Steps the enable object change history

Step 1: Update the control table for the properties to be monitored Step 2: Use spMFUpdateMFilestoMFSQL to update the class table and history Step 3: Schedule spMFUpdateAllncludedinAppTables in an agent to regularly update the history

MFObjectChangeHistory

This table contains the result of the get history operation. Examples of using the result in queries and views are detailed below.

The table columns are the following:
  • ID: Identity of row

  • ObjectType_ID: MFID of the ObjectType

  • Class_ID: MFID of the Class

  • Objid: Objid of the class object

  • MFVersion: Version of the object where the value changed for the property in column Property_ID. Only versions matching the filters on the spMFGetHistory procedure is fetched. Widening the filters may restrict the MFVersions returned. Narrowing the filters will not remove the rows previously fetched for

the object.
  • LastModifiedUtc: The ‘CheckInTimeStamp’ of the specific version for the object. The timestamp is shown in Universal Time.

  • MFLastModifiedBy_ID: MFID of the user. User information is on MFUserAccount or MFLoginAccount.

  • Property_ID: MFID of the property specified in the MFObjectChangeHistoryControl table.

  • Property_Value: value as a string. Interpreting and relating to this value will depend on the type of property. Lookups require special consideration, see below.

  • CreatedOn: timestamp when the row was recreated in the table

Column relationships and conversions

Relations for ObjectType_ID

--object types in change history table
SELECT DISTINCT mot.Name AS objectType FROM [dbo].[MFObjectType] AS [mot]
INNER JOIN [dbo].[MFObjectChangeHistory] AS [moch]
ON mot.[MFID] = moch.[ObjectType_ID]

--getting the object type id for the class

SELECT MC2.MFID class_id, mot.MFID ObjectType_ID, mc2.name Class, mot.name ObjectType FROM [dbo].[MFClass] AS [mc2]
INNER JOIN [dbo].[MFObjectType] AS [mot]
ON mot.id = mc2.[MFObjectType_ID]

Relations for Class_ID

SELECT mc2.name FROM [dbo].[MFClass] AS [mc2]
INNER JOIN [dbo].[MFObjectChangeHistory] AS [moch]
ON mc2.mfid = moch.[Class_ID]
GROUP BY mc2.name

Universal versus local datetime

--understanding dates and times
SELECT SYSDATETIME() AS [SYSDATETIME()]
    ,SYSDATETIMEOFFSET() AS [SYSDATETIMEOFFSET()]
    ,SYSUTCDATETIME() AS [SYSUTCDATETIME()]
    ,CURRENT_TIMESTAMP AS [CURRENT_TIMESTAMP]
    ,GETDATE() AS [GETDATE()]
    ,GETUTCDATE() AS [GETUTCDATE()];

  --adjust for local time (where the time offset is known)
 SELECT TOP 5 [moch].[LastModifiedUtc], DATEADD(HOUR,-5,[moch].[LastModifiedUtc]) EasternTime FROM [dbo].[MFObjectChangeHistory] AS [moch]

Last modified user

SELECT mla.[UserName], [mla].[FullName] FROM [dbo].[MFObjectChangeHistory] AS [moch]
INNER JOIN [dbo].[MFLoginAccount] AS [mla]
ON moch.[MFLastModifiedBy_ID] = mla.[MFID]

Property

SELECT mp.name AS propertyName FROM [dbo].[MFProperty] mp
INNER JOIN [dbo].[MFObjectChangeHistory] AS [moch]
ON mp.[MFID] = moch.[Property_ID]

Property Value

Lookup property values require special consideration as the column will contain the id or comma delimited list of ids rather than the labels. It is best practice to build datasets for reporting and other uses for the change data around specific property types. Combining analysis of change history for diffferent property types simultaneously is more complex. There are 4 types of lookups, each with different considerations:

  • valuelist single and multi select

  • workflow

  • class table object single and multi select

  • workflow state

Workflow

The property value is the MFID of the workflow in the MFWorkflow Table if the property_id = 38

SELECT name, mfid FROM [dbo].[MFWorkflow] AS [mw]
INNER JOIN [dbo].[MFObjectChangeHistory] AS [moch]
ON moch.[Property_Value] = mw.[MFID]
WHERE [moch].[Property_ID] = 38

workflow state

The property value is the MFID of the workflow state in the MFWorkflowState Table if the property_id = 39

SELECT name, mfid FROM [dbo].[MFWorkflowState] AS [mw]
INNER JOIN [dbo].[MFObjectChangeHistory] AS [moch]
ON moch.[Property_Value] = mw.[MFID]
WHERE [moch].[Property_ID] = 39

Valuelist item - single lookup

The property value is the MFID of the Valuelist item in the MFValuelistItem Table. The MFValuelistItem must be joined with the MFValuelist for the specific property to select the correct MFID through the MFProperty Table. Both Valuelist related to the property_ID and the Valuelist Item ID for the Property Value must be matched. See line 10 below.

The samples below have three different approaches to achieve the same objective.

  • The first illustrate the joins based on the base tables.

  • The second use the MFvwMetadataStructure to simplify the relationship

  • The third use a valuelist view. This view is generated using the spMFCreateValuelistLookup

SELECT moch.id,[moch].[ObjID], moch.MFVersion,  moch.[Property_ID], moch.[Property_Value]
, mp.name Property, mvl.name AS Valuelist, mvl.[RealObjectType]
, mvli.name AS Valuelistitem
  FROM [dbo].[MFObjectChangeHistory] AS [moch]
INNER JOIN [dbo].[MFProperty] AS [mp]
ON moch.[Property_ID] = mp.[MFID]
INNER JOIN [dbo].[MFValueList] AS [mvl]
ON mp.[MFValueList_ID] = mvl.[ID]
INNER JOIN [dbo].[MFValueListItems] AS [mvli]
ON moch.[Property_Value] = mvli.[MFID] AND mvli.[MFValueListID] = mvl.[ID]
ORDER BY [moch].[ObjID]

--using the MFvwMetadatastructure

SELECT * FROM [dbo].[MFObjectChangeHistory] AS [moch]
INNER JOIN [dbo].[MFvwMetadataStructure] AS [mfms]
ON [mfms].[Property_MFID] = [moch].[Property_ID] AND moch.[Class_ID] = mfms.[class_MFID]
INNER JOIN [dbo].[MFValueListItems] AS [mvli]
ON mvli.[MFID] = moch.[Property_Value] AND mfms.[Valuelist_ID] = mvli.[MFValueListID]

--creating a valuelist item view for currency

EXEC [dbo].[spMFCreateValueListLookupView] @ValueListName = 'Currency' -- nvarchar(128)
                                          ,@ViewName = 'vwCurrency'      -- nvarchar(128)
                                          ,@Schema = 'Custom'        -- nvarchar(20)
                                          ,@Debug = 0         -- smallint

SELECT * FROM [dbo].[MFObjectChangeHistory] AS [moch]
INNER JOIN [dbo].[MFProperty] AS [mp]
ON moch.[Property_ID] = mp.mfid
INNER JOIN custom.[VLvwCurrency] AS [vlc]
ON vlc.[MFID_ValueListItems] = moch.[Property_Value] AND vlc.[ID_ValueList] = mp.[MFValueList_ID]
ON

valuelist item - multi lookup

When a multi lookup property are used and there are more than one value selected on the property, the values will be displayed as a comma delimited string.

Before the joins above can be applied, the values in the Property Value column must be split to allow for it to be joined the the underlying tables.

Using cross apply with fnMFParseDelimitedString will parse the string and allow joining with its parts. This is illustrated with the second example for valuelist items.

-- working with a multi lookup valuelist

SELECT * FROM [dbo].[MFObjectChangeHistory] AS [moch]
CROSS APPLY [dbo].[fnMFParseDelimitedString]([moch].[Property_Value],',') AS [fmpds]
INNER JOIN [dbo].[MFvwMetadataStructure] AS [mfms]
ON [mfms].[Property_MFID] = moch.[Property_ID] AND moch.[Class_ID] = mfms.[class_MFID]
INNER JOIN [dbo].[MFValueListItems] AS [mvli]
ON mvli.[MFID] = [fmpds].[ListItem] AND mfms.[Valuelist_ID] = mvli.[MFValueListID]

Class table objects or real Object Type objects

Where the property references a real object, such as ‘Customer’, the Property_Value column will reference the objid of the class. In the example below the list show the changes for the Account property which references the MFAccount class table.

SELECT * FROM [dbo].[MFObjectChangeHistory] AS [moch]
INNER JOIN [dbo].[MFvwMetadataStructure] AS [mfms]
ON [mfms].[Property_MFID] = moch.[Property_ID] AND moch.[Class_ID] = mfms.[class_MFID]
INNER JOIN [dbo].[MFAccount] AS [ma]
ON moch.[Property_Value] = ma.[ObjID]
WHERE [mfms].[IsObjectType] = 1

Clearing rows in table

The MFObjectChangeHistory table contains the version history for all the classed and objects and properties for every time the procedure spMFGetHistory is processed. This table is likely to grow very fast if not maintained.

There is no automated process for clearing the history table. It really depends on the specific application and use case for the object history.

In most applications fetching the history for an object is incidental and can be removed after the data was consumed. In other cases this table is constantly consumed for reporting on previous history.

Devising a strategy for deleting records in this table is likely to be different for each class, and could even be different for specific properties on the class.

Adhoc use of the change history can be deleted from the table. Always delete by class. Truncating the entire table may destroy history records of other classes unintentionally.

DELETE FROM [dbo].[MFObjectChangeHistory]
WHERE [Class_ID] IN (SELECT MFID FROM MFClass WHERE [TableName] = 'MFPurchaseInvoice')

MFObjectChangeHistoryUpdateControl

This control table must have an entry for each class table to be included in the change history pull. Each property to be included for the class table must be included in a separate row.

The underlying class table must be created and kept up to date for the change history to work.

Records must be added for your specific requirements in the control table. The update procedure will have no impact if no records are found in this table.

INSERT INTO dbo.MFObjectChangeHistoryUpdateControl(
MFTableName,
ColumnNames)
VALUES
(N'MFCustomer', N'City'),
N'MFCustomer', N'Country_ID'),
N'MFCustomer', N'State_ID'),
N'MFPurchaseInvoice', N'State_ID');

spMFUpdateMfilesToSQL

A new paramater is added to this procedure for including updating of the change history for the class specified class table. By default change history is not included. When the class table is included in the MFObjectChangeHistoryUpdateControl table then it would automatically update the change history for the objects that a) is updated using the main procedure and b) check for any objects where the history is older than the class table and update the history. Note that this works on incremental updates. It will only look for changes history from the date of the previous update.

To update Change history when updating the class table: Set the @WithObjectHistory = 1 This setting will check the MFObjectChangeHistoryUpdateControl table, if the class table has entries in this control table then it would use these entries to call spMFUpdateObjectChangeHistory

Use spMFGetHistory as described below to force a full update of the change history for a table

DECLARE @MFLastUpdateDate SMALLDATETIME,
@Update_IDOut         INT,
@ProcessBatch_ID      INT;

EXEC dbo.spMFUpdateMFilesToMFSQL @MFTableName = 'MFCustomer',
@MFLastUpdateDate = @MFLastUpdateDate OUTPUT,
@UpdateTypeID = 1,
@MaxObjects = null,
@WithObjectHistory = 1,
@Update_IDOut = @Update_IDOut OUTPUT,
@ProcessBatch_ID = @ProcessBatch_ID OUTPUT,
@debug = 0

spMFUpdateAllncludedinAppTables

This procedure no longer calls spmfUpdateObjectChangeHistory directly. It only calls spMFUpdateMfilesToSQL which in turns call spmfUpdateObjectChangeHistory

spmfUpdateObjectChangeHistory

Use spMFUpdateobjectChangeHistory for automating the manual process of using spmfGetHistory. This is an alternative method to spMFUpdateMFilestoMFSQL and may be used on its own. spMFUpdateMfilestoSQL actually calls this procedures as part of its routine.

If @withClasstableupdate is set to 1 then the class table will be updated before the history is pulled.

If only a subset of objects must be updated then the @Objids can be set as a comma delimited string to update only the specific objects’ change history.

DECLARE   @ProcessBatch_ID      INT;

EXEC dbo.spMFUpdateObjectChangeHistory @MFTableName = 'MFcustomer',
@WithClassTableUpdate = 0,
@Objids = null,
@ProcessBatch_ID = @ProcessBatch_ID OUTPUT,
@Debug = 0

spMFGetHistory

spMFGetHistory is the core procedure to fetch the history from M-Files

The operation will get the change history for specific objects and for specific properties. It is not designed to get the change history for every property on the object in a single process. The operation is intended to be used where the target property and the specific object is predetermined.

For instance the change history can be extracted for address changes in all the customers ; the workflow state changes for purchase orders approved in the last month can be extracted.

Volume warning

It is recommended to carefully consider the population of the extract before executing the procedure. It is very easy to request the history for a number of object and return many thousands of results. For instance, getting the history of 4000 customers using 5 properties with an average of 10 versions per customer would produce approx 200 000 history records.

Using filters

Several types of filters are available:

  • Exclude the objects where the data is no longer required or relevant: for instance if the report is targeting the approvals of invoices in the past month then only include the invoices where the MFLastModified date is in the last month by updating the process_id = 5 only on these records.

  • Only include the properties on the object that is relevant for the report. The use of multiple properties should be reduced to the minimum.

  • Only use getting the full history if it is relevant, or the fist time that history is being updated. It would be more productive to initialize the data by getting the full history, and then to update the history by setting the start date or number of days filter.

  • The search string filter can be used to return a result for a specific value of change. For instance to only return the state change where the approval took place, instead of all the state change, set the search string value to the approval state ID.

Using spMFGetHistory has two steps:
  • set the process_id on the class table for the records to be included in the pull.

  • pull the change history for these rows

Set the records to be updated

UPDATE [MFPurchaseInvoice]
SET Process_ID = 5

Pull the change history

DECLARE @RC INT
DECLARE @TableName NVARCHAR(128) = 'MFPurchaseInvoice'
DECLARE @Process_id INT = 5
DECLARE @ColumnNames NVARCHAR(4000) = 'State'
DECLARE @IsFullHistory BIT = 1
DECLARE @NumberOFDays INT
DECLARE @SearchString NVARCHAR(MAX) = null
DECLARE @StartDate DATETIME --= DATEADD(DAY,-1,GETDATE())
DECLARE @ProcessBatch_id INT
DECLARE @Debug INT = 1
DECLARE @Update_ID  INT

EXEC [dbo].[spMFGetHistory] @MFTableName =  @TableName
                       ,@Process_id = @Process_id
                       ,@ColumnNames = @ColumnNames
                       ,@SearchString = null
                       ,@IsFullHistory = @IsFullHistory
                       ,@NumberOFDays = @NumberOFDays
                       ,@StartDate = @StartDate
                       ,@Update_ID = @Update_ID OUTPUT
                       ,@ProcessBatch_id = @ProcessBatch_id OUTPUT
                       ,@Debug = @debug

SELECT * FROM [dbo].[MFProcessBatch] AS [mpb] WHERE [mpb].[ProcessBatch_ID] = @ProcessBatch_id
SELECT * FROM [dbo].[MFProcessBatchDetail] AS [mpbd] WHERE [mpbd].[ProcessBatch_ID] = @ProcessBatch_id

SELECT * FROM [dbo].[MFObjectChangeHistory] AS [moch]