Rendszerindítás vállalati adatokkal – GYIK
Megjegyzés
- novemberi hatállyal:
- A Common Data Service neve mostantól Microsoft Dataverse. A további tudnivalókat lásd: Power Automate blog.
- Néhány terminológiát frissítettünk a Microsoft Dataverse szolgáltatásban. Például az entitás mostantól tábla, a mező pedig oszlop. A további tudnivalókat lásd: Terminológia frissítései.
Ezt a témakört hamarosan frissítjük a legújabb terminológia szerint.
Miért van szükség rendszerindításra?
Előfordulhat, hogy üzleti adatokkal rendelkező Dataverse alkalmazáspéldányt vagy más Dynamics 365-alkalmazáspéldányt használ, és engedélyezni szeretné a kettős írású kapcsolatot hozzá. Ebben az esetben a kettős írású kapcsolat engedélyezése előtt rendszerindítást kell végezni a Dataverse- vagy a másik Dynamics 365-alkalmazásadatainál a vállalati információkkal.
Mikor kell használni a rendszerindítást?
A kettős írású táblaleképezések engedélyezése előtt kell rendszerindítást használni (az 5. lépés során).
Ha a Finance and Operations alkalmazás és a Dataverse vagy másik Dynamics 365-alkalmazás példányai között be szeretné állítani a kettős írású kapcsolatot, jelentkezzen be a Finance and Operations alkalmazásba rendszergazdaként.
Lépjen az Adatkezelés modulba, és kattintson a Kettős írás gombra. Ez elindítja az Adatintegrátort.
Hozzon létre kettős írású kapcsolatot legalább egy vállalathoz.
Engedélyezze a Cdm_companies táblaleképezést. Ez szinkronizálja a vállalatokat a Finance and Operations alkalmazásból a következőbe: Dataverse.
Futtassa a rendszerindítási mintakódot a Dataverse vagy a másik Dynamics 365-alkalmazás példányában.
Amikor a rendszerindítás után a rendszer készen áll az élő szinkronizálásra, engedélyezze az táblaleképezést.
Az táblaleképezés engedélyezése elindítja a kezdeti adatszinkronizálási műveletet az engedélyezett táblaleképezésekhez. A program szinkronizálja a kettős írású kapcsolatnál kiválasztott vállalatokra vonatkozó adatokat a Finance and Operations és a Dataverse között.
Hogyan használható a kódminta?
A mintakód egy olyan C#-alkalmazás, amely betölthető a Visual Studio rendszerben. NuGet-csomagfüggőségekre van szükség a Dataverse szoftverfejlesztői készlethez (SDK), ha szokásos Visual Studio-eszközökkel szeretne frissíteni.
Miután kitömörítette és megnyitotta a megoldást a Visual Studio rendszerben, illetve helyreállította a NuGet-csomagokat, keresse meg a TENNIVALÓ részt a kódban. A vállalati adatok rendszerindítására vonatkozó összes döntés esetében TENNIVALÓ jegyzet tartozik, valamint a szabályos megvalósítást bemutató mintakód.
A mintakódban csak a vállalat entitássorainak kategorizálására szolgáló egyik mód jelenik meg. Ha megváltoztatja a logikát a TENNIVALÓ szakaszokban, egyéni kategorizálást hozhat létre.
Mi várható?
Alapértelmezés szerint a mintapályázattal az üzleti egységek és a vállalat közötti kódleképezések tára adható meg. Az OwningBusinessUnit mező használatával rendszerindított entitás automatikusan a megadott vállalatot használja. Az OwningBusinessUnit mező nélküli entitások (pl. egy termék) üres üzletiegység-értékkel való leképezés alapján állítja be a vállalatot.
A konzolpályázat egy paramétert vár: a –szimuláció vagy az –alkalmaz paramétert. Ha a –szimuláció parancssori paramétert használja, akkor semmilyen adat nem frissül. Csak simulation_.csv-fájlok jönnek létre, ugyanabban a könyvtárban, mint amelyikben az eszköz van; minden olyan entitáshoz egy, amelyik frissülne. Ezeket a fájlokat munka közben iteratívan ellenőrizheti, hogy a kód csakugyan a várt módon frissítse a vállalat értékeit.
Ha befejezte a szimulált frissítéseket, használja az –alkalmaz paramétert. Ez frissíti az összes olyan sort, amelyen jelenleg hibás a vállalati érték, egyszerre 1000 soros kötegekben (alapértelmezés szerint). A kód a megadott módon idempotens, vagyis újra futtatható, és csak a helytelenül hozzárendelt vállalatok lesznek frissítve. Az –alkalmaz paraméter futtatásakor a kód az elvégzett változtatások CSV-fájljait adja eredményként; a fájlok neve applied_.csv.
using Microsoft.Crm.Sdk.Messages;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Messages;
using Microsoft.Xrm.Sdk.Query;
using Microsoft.Xrm.Tooling.Connector;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
namespace BootstrapCompany
{
/// <summary>
/// Application to bootstrap the company field on existing rows in CDS in preparation for integration to Finance and Operations.
/// </summary>
/// <remarks>
/// This application assumes that the target companies already exist in the CDS environment in the cdm_Company table and are
/// identified by their company code. It also assumes that the current owning business unit of each row should be used
/// to categorize by company. This logic can easily be updated to utilize alternate sources of categorization including
/// custom tables, teams, custom fields on tables, or any other data. This code is provided only as a sample.
///
/// To utilize this code, update each of the locations currently denoted with a TODO statement.
///
/// This code is provided AS IS with no warranties or guarantees, and confers no rights.
/// </remarks>
public class Program
{
/// <summary>
/// The number of rows to query and update in CDS in a single operation.
/// </summary>
/// <remarks>
/// The larger this number, the fewer calls will need to be made, so the faster the updates
/// will complete. However, larger batch sizes are more likely to cause contention. Additionally,
/// when SQL exceeds some threshold of locks (generally around 5,000), it will escalate to
/// an entire table lock, which blocks all other activity in the live system on this table. As
/// such, a batch size of around 1,000 is relatively fast, while also relatively safe in terms
/// of contention and transaction time.
/// </remarks>
const int requestBatchSize = 1000;
/// <summary>
/// The number of faults that may be seen in CDS before the operation is aborted and an exception is thrown.
/// </summary>
/// <remarks>
/// An occassional error due to contention when updating large tables in production is expected, so by default
/// errors are logged and skipped. However, if a large number of errors are seen, ignoring those errors
/// in subsequent batches gets expensive, and is usually indicative of a larger issue that should be addressed
/// before continuing. Faulted requests are *not* retried, but would be picked up in a subsequent run of this script.
/// </remarks>
const int maxFaultThreshold = 100;
/// <summary>
/// The maximum number of rows per business unit to export when simulating.
/// </summary>
/// <remarks>
/// During simulation, queries are not batched since doing so would require ordering and so be slightly
/// different from the actual execution logic. To keep this the same between both paths, simulates are
/// not batched and so a separate maximum number of rows per business unit can be specified.
/// </remarks>
const int maxSimulateRecordsPerBusinessUnit = 10000;
/// <summary>
/// Whether or not operations should continue if any errors are encountered.
/// </summary>
/// <remarks>
/// This is different than setting maxFaultThreshold = 0, since the first batch of updates will be processed
/// together. If continueOnError is true and maxFaultThreshold is 0, it is possible that multiple errors may
/// be encountered and at the same time some rows successfully updated. In a healthy system when updating
/// a higher number of rows, an occasional spurious error is expected, so it is recommended this be left as true.
/// </remarks>
const bool continueOnError = true;
#region private variables
private static Dictionary<string, EntityReference> cachedCompanyReferences = new Dictionary<string, EntityReference>();
#endregion
/// <summary>
/// The main execution loop of the program.
/// </summary>
/// <param name="args">No arguments are expected.</param>
static void Main(string[] args)
{
if (args.Length != 1 && args[0] != "-simulate" && args[0] != "-apply")
{
Console.WriteLine("Usage: BootstrapCompany -simulate");
Console.WriteLine(" BootstrapCompany -apply");
Console.WriteLine("The -simulate flag will create a file called simulation.csv in the working");
Console.WriteLine("directory, but will not change any data. The -apply flag will update live data");
Console.WriteLine("in the same way that was demonstrated in the simulation.");
return;
}
bool isSimulate = args[0].Equals("-simulate", StringComparison.OrdinalIgnoreCase);
// Delete the simulation or applied files if existing
foreach (string existingSimulate in Directory.EnumerateFiles(Directory.GetCurrentDirectory(), $"{(isSimulate ? "simulation" : "applied")}_*.csv"))
{
File.Delete(existingSimulate);
}
IOrganizationService orgService;
// TODO: Provide your connection string details for your environment
CrmServiceClient cdsConnection = new CrmServiceClient("AuthType=Office365;Username=youraliashere@yourdomainhere.com;Password=yourpasswordhere;URL=https://yourorganizationurlhere.crm.dynamics.com/;");
orgService = (IOrganizationService)cdsConnection.OrganizationWebProxyClient != null ? (IOrganizationService)cdsConnection.OrganizationWebProxyClient : (IOrganizationService)cdsConnection.OrganizationServiceProxy;
if (orgService != null)
{
// Get the current user ID to verify the connection was successful
Guid userid = ((WhoAmIResponse)orgService.Execute(new WhoAmIRequest())).UserId;
if (userid != Guid.Empty)
{
Console.WriteLine("Connection Successful!");
}
// TODO: Provide a mapping of OwningBusinessUnit name to cdm_Company company ID. You can reuse
// the same company ID for multiple business units if desired. In this example, it assumes that
// the business unit named "USMF" is related to the company "USMF". If all rows were owned
// by the same root business unit, then the first field in the dictionary should be set to the
// name of the root business unit, usually the same value as the organization (eg, "Contoso").
Dictionary<string, string> businessUnitToCompanyMapping = new Dictionary<string, string>()
{
{ "", "USMF" }, // The default mapping to use for any entity that doesn't have an owningbusinessunit field
{ "USMF", "USMF" },
{ "FRRT", "FRRT" },
};
// TODO: Provide a list of tables for which the company field should be backfilled based
// on owning business unit. The list below represents all existing tables for which a cdm_Company
// lookup field was added as part of the Finance and Operations dual write project.
BatchUpdateEntity(orgService, "account", "msdyn_company", businessUnitToCompanyMapping, true, isSimulate, "accountnumber", "name");
BatchUpdateEntity(orgService, "contact", "msdyn_company", businessUnitToCompanyMapping, true, isSimulate, "fullname");
// ... Add more here
// Note, the product entity does not have an owningbusinessunit field like most other tables, so
// assigning company by Business Unit is not applicable. In this case, whichever mapping specifies an
// empty business unit will be used to categorize tables without an owningbusinessunit field.
BatchUpdateEntity(orgService, "product", "msdyn_companyid", businessUnitToCompanyMapping, false, isSimulate, "productnumber");
}
else
{
Console.WriteLine("Connection failed...");
}
Console.WriteLine("Done");
Console.ReadLine();
}
/// <summary>
/// Updates all incorrectly assigned company relationships for the specified entity.
/// </summary>
/// <param name="orgService">The connection to CDS.</param>
/// <param name="entityName">The logical name of the entity to update.</param>
/// <param name="companyFieldName">The physical name of the field in the entity being updated which contains the cdm_Company id.</param>
/// <param name="businessUnitToCompanyMapping">A dictionary of business unit name to company code.</param>
/// <param name="hasOwningBusinessUnit">true if the entity has an owningbusinessunit field; otherwise, false.</param>
/// <param name="isSimulate">true to simulate output; otherwise, false.</param>
/// <param name="fieldsToExport">A set of fields to export into a CSV for this entity if simulating.</param>
/// <returns>true if the entity was successfully processed without any errors; otherwise, false.</returns>
private static bool BatchUpdateEntity(
IOrganizationService orgService,
string entityName,
string companyFieldName,
Dictionary<string, string> businessUnitToCompanyMapping,
bool hasOwningBusinessUnit,
bool isSimulate,
params string[] fieldsToExport)
{
List<Guid> faultedIds = new List<Guid>();
int totalRecordsProcessed = 0;
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
string fileName = isSimulate ? "simulation" : "applied";
StreamWriter simulationWriter = new StreamWriter(Path.Combine(Directory.GetCurrentDirectory(), $"{fileName}_{entityName}.csv"), true);
simulationWriter.Write("EntityName,EntityId,");
foreach (string fieldToExport in fieldsToExport)
{
simulationWriter.Write($"{fieldToExport},");
}
simulationWriter.WriteLine("BusinessUnit,NewCompanyId");
// Process each mapped business unit individually
foreach (string businessUnitName in businessUnitToCompanyMapping.Keys)
{
Console.WriteLine("Updating any {0} rows for business unit {1} to company {2}...", entityName, businessUnitName, businessUnitToCompanyMapping[businessUnitName]);
// The empty business unit value is only applicable for tables without an owning business unit field
if (hasOwningBusinessUnit && string.IsNullOrEmpty(businessUnitName))
{
continue;
}
else if (!hasOwningBusinessUnit && !string.IsNullOrEmpty(businessUnitName))
{
continue;
}
var companyRef = GetCompanyReference(orgService, businessUnitToCompanyMapping[businessUnitName]);
// Iteratively loop in batches to keep transaction lock size small
bool moreRecordsExist = true;
while (moreRecordsExist)
{
moreRecordsExist = false;
// Find the first batch of rows for this business unit with the wrong company ID. Ordering
// is not explicity specified, but SQL will most likely process based on the index starting with
// company ID, since all new company ID fields added for Finance and Operations integration have
// also added a new index starting with company ID. Explicitly specifying order would reduce the
// query plan options for SQL and introduce unnecessary overhead.
QueryExpression query = new QueryExpression(entityName);
query.ColumnSet.AddColumns(companyFieldName);
foreach (string fieldToExport in fieldsToExport)
{
query.ColumnSet.AddColumn(fieldToExport);
}
query.Criteria.AddCondition(companyFieldName, ConditionOperator.NotEqual, companyRef.Id);
// TODO: Uncomment the line below if you only want to fill in companies that are empty
// as opposed to the line above which updates the company any time it differs from the
// desired value
// query.Criteria.AddCondition(companyFieldName, ConditionOperator.Equal, Guid.Empty);
if (isSimulate)
{
// During simulation, get as a single block of rows to avoid positioning complexities
query.TopCount = maxSimulateRecordsPerBusinessUnit;
}
else
{
// Only batch rows during actual application, otherwise retrieve all as a single operation
query.TopCount = requestBatchSize + faultedIds.Count;
}
// For tables with an owning business unit, join based on business unit name
if (hasOwningBusinessUnit)
{
// TODO: Replace this logic with different algorithms to determine the correct company
// in situations where business unit is not the best way to categorize.
LinkEntity linkEntity = query.AddLink("businessunit", "owningbusinessunit", "businessunitid", JoinOperator.Inner);
linkEntity.Columns.AddColumns("name");
linkEntity.LinkCriteria.AddCondition("name", ConditionOperator.Equal, businessUnitName);
}
var multipleRequest = new ExecuteMultipleRequest()
{
Settings = new ExecuteMultipleSettings()
{
ContinueOnError = true,
ReturnResponses = true
},
Requests = new OrganizationRequestCollection()
};
EntityCollection result = orgService.RetrieveMultiple(query);
int rowsAddedToBatch = 0;
foreach (var entity in result.Entities)
{
// Skip any previously faulted ID's. These values will be re-queried with each batch
// which is inefficient, but is more efficient than passing hundreds of ID values to
// the underlying SQL query to be skipped at the database level (assuming the
// max fault count is relatively small).
if (faultedIds.Contains(entity.Id))
{
continue;
}
entity.Attributes[companyFieldName] = companyRef;
UpdateRequest updateRequest = new UpdateRequest()
{
Target = entity
};
simulationWriter.Write($"{entityName},{entity.Id},");
foreach (string fieldToExport in fieldsToExport)
{
simulationWriter.Write($"{entity.Attributes[fieldToExport]},");
}
simulationWriter.WriteLine($"{businessUnitName},{businessUnitToCompanyMapping[businessUnitName]}");
// Only add the update request when applying for real
if (!isSimulate)
{
multipleRequest.Requests.Add(updateRequest);
}
rowsAddedToBatch++;
Console.Write(".");
}
totalRecordsProcessed += rowsAddedToBatch;
if (rowsAddedToBatch > 0 && !isSimulate)
{
Console.Write("Sending {0} updates in a batch", rowsAddedToBatch);
var updateResult = orgService.Execute(multipleRequest) as ExecuteMultipleResponse;
moreRecordsExist = true;
Console.WriteLine(" done");
// If any faults are encountered, flag those IDs to not be processed again
// in subsequent batches.
if (updateResult.IsFaulted)
{
foreach (var response in updateResult.Responses)
{
if (response.Fault != null)
{
Console.WriteLine(response.Fault);
faultedIds.Add(((UpdateRequest)multipleRequest.Requests[response.RequestIndex]).Target.Id);
if (faultedIds.Count > 100)
{
throw new ApplicationException("Excessive number of update failures, aborting operation");
}
}
}
}
}
else
{
Console.WriteLine("No {0} rows remain to be updated for {1}->{2}", entityName, businessUnitName, businessUnitToCompanyMapping[businessUnitName]);
}
}
}
simulationWriter.Close();
simulationWriter = null;
stopwatch.Stop();
Console.WriteLine("Processed {0} rows for the {1} entity in {2}ms.", totalRecordsProcessed, entityName, stopwatch.ElapsedMilliseconds);
return (faultedIds.Count == 0);
}
/// <summary>
/// Gets an entity reference to the company with the specified ID if one exists.
/// </summary>
/// <param name="orgService">The CDS connection.</param>
/// <param name="companyId">The company ID to search for.</param>
/// <returns>An entity reference if one exists; otherwise, null.</returns>
private static EntityReference GetCompanyReference(IOrganizationService orgService, string companyId)
{
if (cachedCompanyReferences.ContainsKey(companyId))
{
return cachedCompanyReferences[companyId];
}
QueryExpression query = new QueryExpression("cdm_company");
query.ColumnSet.AddColumns("cdm_companyid");
query.Criteria.AddCondition("cdm_companycode", ConditionOperator.Equal, companyId);
query.TopCount = 1;
EntityCollection result = orgService.RetrieveMultiple(query);
EntityReference entityRef = null;
foreach (var entity in result.Entities)
{
entityRef = entity.ToEntityReference();
break;
}
cachedCompanyReferences[companyId] = entityRef;
return entityRef;
}
}
}
Visszajelzés
https://aka.ms/ContentUserFeedback.
Hamarosan elérhető: 2024-ben fokozatosan kivezetjük a GitHub-problémákat a tartalom visszajelzési mechanizmusaként, és lecseréljük egy új visszajelzési rendszerre. További információ:Visszajelzés küldése és megtekintése a következőhöz: