<Subjob Id="VKOCustomerMarketingInformation" AxTableName="VKOCustomerMarketingInformation" TargetTableName="VKOCustomerMarketingInformation" TargetTableSchema="ext" ReplicationCounterFieldName="REPLICATIONCOUNTERFROMORIGIN" IsUpload="true"> <ScheduledByJobs> <ScheduledByJob>P-0001</ScheduledByJob> </ScheduledByJobs> <AxFields> <Field Name="EmailAddress" /> <Field Name="CustomerName" /> <Field Name="Salutation" /> <Field Name="Channel" /> <Field Name="ReplicationCounterFromOrigin" /> </AxFields> </Subjob>ReplicationCounterFromOrigin (RetailReplicationCounter EDT) field is mandatory, it is auto increment field (identity), which value will be used to save last pulled record id.
USE [AxDB] GO SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[ext].[VKOCUSTOMERMARKETINGINFORMATION]') AND type in (N'U')) BEGIN Create Table [ext].[VKOCUSTOMERMARKETINGINFORMATION]( EMAILADDRESS NVARCHAR(50) NOT NULL, SALUTATION NVARCHAR(50) NOT NULL, CUSTOMERNAME NVARCHAR(50) NOT NULL, CREATEDDATE DATETIME NOT NULL, [CHANNEL] [bigint] NOT NULL, [REPLICATIONCOUNTERFROMORIGIN] [int] IDENTITY(1,1) NOT NULL, [DATAAREAID] [nvarchar](4) NOT NULL, [ROWVERSION] [timestamp] NOT NULL, CONSTRAINT [PK_VKOCUSTOMERMARKETINGINFORMATION] PRIMARY KEY CLUSTERED ( [CHANNEL], [REPLICATIONCOUNTERFROMORIGIN] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] ) ON [PRIMARY] END GO GRANT SELECT ON [ext].[VKOCUSTOMERMARKETINGINFORMATION] TO [UsersRole]; GO GRANT INSERT, SELECT ON OBJECT::[ext].[VKOCUSTOMERMARKETINGINFORMATION] TO [DataSyncUsersRole]; GOBasically that it, if you do not need data post processing you can initialize retail and run P-0001 job to see how AX pulls data into table from channel.
Unfortunately, 'X' table and static table method InsertToRegularTable is no longer valid for D365.There are a lot of garbage left around this approach in D365 but it is not used.
AX7 processes all data in bulk way you will not be able to fall to record by record processing.
In my case there was requirement to store in AX all unique customer email addresses with some additional information. The newest data is valid. As AX supports pull requests only in a transaction way we had to create table in POS to store "transactions". If customer information was taken several times, it means that table will contain several records regardless of email existence.
In AX I had to create similar to POS table, which is used only for pulling data. Actually, I set it as TempDB as I do not need to store all "these transaction". Then on post processing I'm selecting records from "regular" table (VKOCustomerMarketingInformation - which I made temporary, but framework consider it as regular) and making either insert or update to my target table (VKOCustMarketing) where all emails are unique.
To have a post processing you need to set post handler to RetailConnSCDataBulkCopy.insertIntoTargetTable:
/// <summary> /// The <c>VKORetailConnSCPostProcessing</c> class performs post processing of data after bulk copy to regular tables in AX. /// </summary> class VKORetailConnSCPostProcessing { /// <summary> /// Executed after data gets into AX table /// </summary> /// <param name="args"></param> [PostHandlerFor(classStr(RetailConnSCDataBulkCopy), methodStr(RetailConnSCDataBulkCopy, insertIntoTargetTable))] public static void RetailConnSCDataBulkCopy_Post_insertIntoTargetTable(XppPrePostArgs args) { common targetTempTableCursor = args.getArgNum(1); switch (targetTempTableCursor.TableId) { case tableNum(VKOCustomerMarketingInformation): VKORetailConnSCPostProcessing::customerMarketingInformationPostProcessing(targetTempTableCursor); break; } } public static void customerMarketingInformationPostProcessing(VKOCustomerMarketingInformation _tempTable) { VKOCustMarketing target; VKOCustomerMarketingInformation tmpSource; int maxRetry = 5; int retryCount = 0; int i = 0; container companies; DataAreaID le; while select crossCompany DataAreaId from _tempTable group by DataAreaId { companies += _tempTable.dataAreaId; } try { ttsBegin; ++retryCount; for (i = 1; i <= conLen(companies); ++i) { le = conPeek(companies, i); changecompany(le) { Map keyMap = new Map(types::String, types::Int64); target = null; tmpSource = null; tmpSource.setTempDB(); tmpSource.linkPhysicalTableInstance(_tempTable); while select tmpSource outer join forUpdate target order by tmpSource.ReplicationCounterFromOrigin where target.EmailAddress == tmpSource.EmailAddress { if (keyMap.exists(tmpSource.EmailAddress) && !target) { // there are two records related to same email. Initially email does not exist // On the first run it will insert record, on second it will fail as duplicate // So in such cases we will have to select target table, to update it select firstonly forUpdate target where target.RecId == keyMap.lookup(tmpSource.EmailAddress); } target.EmailAddress = tmpSource.EmailAddress; target.Salutation = tmpSource.Salutation; target.CustomerName = tmpSource.CustomerName; target.Channel = tmpSource.Channel; target.write(); if (!keyMap.exists(tmpSource.EmailAddress)) { keyMap.insert(tmpSource.EmailAddress, target.RecId); } } } } ttsCommit; } catch(Exception::Error) { if (retryCount >= maxRetry) { throw Exception::Error; } else { retry; } } } }
If you need to manipulate with ReplicationCounterFromOrigin. The information about last value might be stored in RetailCDXUploadCounter table.
Using its static methods updateCounter/getCounter you might be able to get/update the value. However, I did not have a need to use it. And it is also possible that it is no longer used in AX7. You can get example of its usage in RetailTransactionTableX table:
public static void UpdateUploadCounter(RetailTransactionTableX cursor) { RetailTransactionTableX cursor2; RefRecId dataStoreRecID; ; cursor2.linkPhysicalTableInstance(cursor); while select crossCompany Origin, maxof(ReplicationCounterFromOrigin) from cursor2 group by Origin { dataStoreRecID = str2int64(cursor2.Origin); RetailCDXUploadCounter::updateCounter(dataStoreRecID, tableStr(RetailTransactionTable), cursor2.ReplicationCounterFromOrigin); } }
There is also [crt].[TABLEREPLICATIONLOG] table, whicl also hold min/max filter, it is used in RetailCdxChannelDbDirectAccess.getSourceRequestHeader
Please be aware that in AX7 approach with 'X' table and it's methods is no longer used. The code is still in the application, but it is not working.
You can see it by comparing \Classes\RetailConnSCUploadManager\insertToRegularTable realization of AX2012 and AX7.
DaxOnline.org is free platform that allows you to quickly store and reuse snippets, notes, articles related to Dynamics AX.
Authors are allowed to set their own AdSense units and "buy me a coffee" link.
Join us.