Skip to content

Commit 690fdcd

Browse files
Migrate data from one table to another (ServiceNowDevProgram#704)
1 parent ec515a6 commit 690fdcd

File tree

2 files changed

+382
-0
lines changed

2 files changed

+382
-0
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Migrate data from one table to another
2+
3+
Sometimes we have to migrate data from one table to another but there has been a lot of customizations on the existing table including the custom columns.
4+
5+
In order to ensure there is no data loss while migrating data, we must have one backup table **sn_data_fix_backup** where we push data before starting migration and there will be no data loss even in the case of any failure. Below is the xml deinition of the backup table.
6+
7+
```
8+
<?xml version="1.0" encoding="UTF-8"?><database>
9+
<element label="Data Fix Backup" max_length="40" name="sn_data_fix_backup" sizeclass="0" type="collection" create_access="true" read_access="true" delete_access="true" update_access="true">
10+
<element label="Record ID" max_length="32" name="record_id" type="GUID"/>
11+
<element choice="1" label="State" max_length="40" name="state" type="choice">
12+
<choice>
13+
<element label="Processed" sequence="10" value="processed"/>
14+
<element label="Unprocessed" sequence="20" value="unprocessed"/>
15+
</choice>
16+
</element>
17+
<element label="Table name" max_length="80" name="table_name" type="table_name"/>
18+
<element label="Values" max_length="30000" name="values" type="string"/>
19+
</element>
20+
</database>
21+
```
22+
23+
One more thing to note here, By default we are inserting record with `Unprocessed` state and once we successfully migrated the record then only we move its state to `Processed` so Customers can easily identify which records has not been migrated/processed yet.
24+
25+
We are also handling Custom column customization in `cloneColumn` function of the script where we are creating custom columns on the target table.
26+
27+
If we want to completely deprecate the previous table then we have to update the fields where the previous table is being refered so we are handling this in `updateDictionaryReferences` function and once migartion is completed, we are deprecating the previosu table in `deprecateTable` function.
28+
29+
We are using `GlideRecordClassSwitcher`function to migrate records from one table to another but this will one migrate data of the columns which are common in both the tables heirarchy so for other fields we have to again populate them again. One important thing to note here is that `GlideRecordClassSwitcher` automatically remove record from the previous table and this will not impact `cascade_rule` of the other tables and **will create record in the new table with the same sys id** so we don't have to worry about references where the record has being references.
Lines changed: 353 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,353 @@
1+
var BACKUP_TABLE_NAME = "sn_data_fix_backup";
2+
var UNPROCESSED_BACKUP_STATE = 0;
3+
var PROCESSED_BACKUP_STATE = 1;
4+
5+
var sourceTable = "";
6+
var targetTable = "";
7+
var alternateMapping = { external_id: "model_number" };
8+
var excludeColumns = ["external_id", "source"];
9+
10+
// migrate(sourceTable, targetTable, alternateMapping, excludeColumns);
11+
12+
function migrate(sourceTable, targetTable, alternateMapping, excludeColumns) {
13+
gs.info("Upgrade: Migrating '{0}' data.", sourceTable);
14+
15+
if (updateDictionaryReferences(sourceTable, targetTable)) {
16+
migrateData(sourceTable, targetTable, alternateMapping, excludeColumns);
17+
18+
if (validateMigration(sourceTable)) deprecateTable(sourceTable);
19+
else gs.error("Upgrade: Migration of '{0}' was not successful so not deprecating the table.", sourceTable);
20+
}
21+
22+
gs.info("Upgrade: '{0}' migration completed.", sourceTable);
23+
}
24+
25+
function validateMigration(sourceTable) {
26+
gs.info("Upgrade: Validating migration for '{0}'.", sourceTable);
27+
28+
try {
29+
var sourceGr = new GlideRecord(sourceTable);
30+
sourceGr.query();
31+
32+
if (sourceGr.getRowCount() != 0) {
33+
gs.info("Upgrade: Found {0} records in the old '{1}' table.", sourceGr.getRowCount(), sourceTable);
34+
return false;
35+
}
36+
37+
gs.info("Upgrade: Successfully validated migration of '{0}'.", sourceTable);
38+
return true;
39+
} catch (ex) {
40+
gs.error("Upgrade: Exception occurred while validating migration for '{0}'.", sourceTable);
41+
}
42+
}
43+
44+
function updateDictionaryReferences(sourceTableName, targetTableName) {
45+
gs.info("Upgrade: Starting Dictionary update where '{0}' is being referenced.", sourceTableName);
46+
47+
try {
48+
// Creating GlideRecord object to access records from sys_dictionary
49+
var dictionaryGr = new GlideRecord("sys_dictionary");
50+
51+
// Using addEncodedQuery to filter the required records
52+
dictionaryGr.addEncodedQuery("reference=" + sourceTableName);
53+
dictionaryGr.query();
54+
55+
// Looping through each record and updating 'reference' field
56+
while (dictionaryGr.next()) {
57+
dictionaryGr.setValue("reference", targetTableName);
58+
dictionaryGr.update();
59+
}
60+
61+
gs.info("Upgrade: Completed Dictionary update where '{0}' is being referenced.", sourceTableName);
62+
63+
return true;
64+
} catch (ex) {
65+
gs.error(
66+
"Upgrade: Exception occurred during Dictionary update where '{0}' is being referenced: {1}",
67+
sourceTableName,
68+
ex
69+
);
70+
71+
return false;
72+
}
73+
}
74+
75+
function deprecateTable(tableName) {
76+
gs.info("Upgrade: starting deprecating '{0}'.", tableName);
77+
78+
var deprecationLabel = " (deprecated)";
79+
var dictGr = new GlideRecord("sys_documentation");
80+
dictGr.addQuery("name", tableName);
81+
dictGr.addQuery("element", "");
82+
dictGr.addEncodedQuery("labelNOT LIKE(deprecated)");
83+
dictGr.query();
84+
85+
if (dictGr.next()) {
86+
dictGr.setValue("label", dictGr.label + deprecationLabel);
87+
dictGr.setValue("plural", dictGr.plural + deprecationLabel);
88+
dictGr.setValue("hint", dictGr.hint + deprecationLabel);
89+
dictGr.update();
90+
gs.info("Upgrade: Successfully deprecated '{0}'", tableName);
91+
} else {
92+
gs.info("Upgrade: No table '{0}' found to deprecate", tableName);
93+
}
94+
}
95+
96+
function cloneColumn(sourceTableName, targetTableName, columnName) {
97+
// Get the source column's record
98+
var sourceColumnGR = new GlideRecord("sys_dictionary");
99+
sourceColumnGR.addQuery("name", sourceTableName);
100+
sourceColumnGR.addQuery("element", columnName);
101+
sourceColumnGR.query();
102+
103+
if (sourceColumnGR.next()) {
104+
// Create a new sys_dictionary record
105+
var colLabel = sourceColumnGR.getValue("column_label");
106+
var colName = sourceColumnGR.getValue("element");
107+
var colType = sourceColumnGR.getValue("internal_type");
108+
var colMaxLength = sourceColumnGR.getValue("max_length");
109+
var colReference = sourceColumnGR.getValue("reference");
110+
var colDefaultValue = sourceColumnGR.getValue("default_value");
111+
var colScopeID = sourceColumnGR.getValue("sys_scope");
112+
SncTableEditor.createElement(
113+
targetTableName,
114+
colLabel,
115+
colName,
116+
colType,
117+
colMaxLength,
118+
colReference,
119+
colDefaultValue,
120+
colScopeID
121+
);
122+
123+
var newDictionaryGR = new GlideRecord("sys_dictionary");
124+
newDictionaryGR.addQuery("element", colName);
125+
newDictionaryGR.addQuery("name", targetTableName);
126+
newDictionaryGR.query();
127+
128+
var excludeFields = [
129+
"name",
130+
"column_label",
131+
"element",
132+
"internal_type",
133+
"max_length",
134+
"reference",
135+
"default_value",
136+
"sys_scope",
137+
"sys_update_name",
138+
];
139+
140+
if (newDictionaryGR.next()) {
141+
newDictionaryGR.setWorkflow(false);
142+
// Loop through all attributes of the source column
143+
var all_fields = sourceColumnGR.getFields();
144+
145+
for (var i = 0; i < all_fields.size(); i++) {
146+
var fieldName = all_fields.get(i).getName();
147+
148+
if (excludeFields.indexOf(fieldName) != -1) {
149+
continue;
150+
}
151+
152+
var fieldValue = sourceColumnGR.getValue(fieldName);
153+
// Set the attribute value in the new sys_dictionary record
154+
newDictionaryGR.setValue(fieldName, fieldValue);
155+
}
156+
157+
// Insert the new sys_dictionary record
158+
newDictionaryGR.update();
159+
160+
gs.info("Upgrade: sys_dictionary record updated with sys_id '{0}'.", newDictionaryGR.getUniqueValue());
161+
}
162+
} else {
163+
gs.error("Upgrade: Source column not found with name '{0}' on table '{1}'.", columnName, sourceTableName);
164+
}
165+
}
166+
167+
function getListOfColumnNames(table) {
168+
var columnGR = new GlideRecord(table);
169+
columnGR.query();
170+
171+
var fields = columnGR.getFields();
172+
var columns = [];
173+
174+
for (var i = 0; i < fields.size(); i++) {
175+
var fieldName = fields.get(i).getName();
176+
columns.push(fieldName);
177+
}
178+
179+
return columns;
180+
}
181+
182+
// function to save the backup of the table records as json string.
183+
function backupRecord(sourceGr, sourceColumns) {
184+
try {
185+
var values = {};
186+
187+
for (var i = 0; i < sourceColumns.length; i++) {
188+
values[sourceColumns[i]] = sourceGr.getValue(sourceColumns[i]);
189+
}
190+
191+
var backupTableGr = new GlideRecord(BACKUP_TABLE_NAME);
192+
backupTableGr.initialize();
193+
backupTableGr.setValue("record_id", sourceGr.getUniqueValue());
194+
backupTableGr.setValue("table_name", sourceGr.getValue("sys_class_name"));
195+
backupTableGr.setValue("state", UNPROCESSED_BACKUP_STATE);
196+
backupTableGr.setValue("values", JSON.stringify(values));
197+
backupTableGr.insert();
198+
gs.info(
199+
"Upgrade: Successfully created backup record of '{0}' table with sys_id '{1}'",
200+
sourceGr.getValue("sys_class_name"),
201+
sourceGr.getUniqueValue()
202+
);
203+
204+
return backupTableGr;
205+
} catch (ex) {
206+
gs.error("Upgrade: Exception occurred while creating backup record: {0}", ex);
207+
}
208+
209+
return false;
210+
}
211+
212+
function backupTable(sourceTableName) {
213+
gs.info("Upgrade: Starting backing up records of '{0}' table", sourceTableName);
214+
215+
try {
216+
var sourceColumns = getListOfColumnNames(sourceTableName);
217+
var backupGr = new GlideRecord(sourceTableName);
218+
backupGr.query();
219+
220+
while (backupGr.next()) {
221+
backupRecord(backupGr, sourceColumns);
222+
}
223+
224+
gs.info("Upgrade: Successfully backup records of '{0}' table.", sourceTableName);
225+
226+
return true;
227+
} catch (ex) {
228+
gs.error("Upgrade: Exception occurred while backing up data for '{0}' table", sourceTableName);
229+
return false;
230+
}
231+
}
232+
233+
function migrateData(sourceTableName, targetTableName, alternateMapping, skipColumns) {
234+
try {
235+
if (!backupTable(sourceTableName)) return;
236+
237+
var sourceColumns = getListOfColumnNames(sourceTableName);
238+
var targetColumns = getListOfColumnNames(targetTableName);
239+
var excludeColumns = skipColumns.concat(["sys_class_name", "sys_update_name"]);
240+
241+
for (var i = 0; i < sourceColumns.length; i++) {
242+
if (excludeColumns.indexOf(sourceColumns[i]) != -1) {
243+
continue;
244+
}
245+
246+
if (targetColumns.indexOf(sourceColumns[i]) == -1) {
247+
var targetColumnMapping = alternateMapping[sourceColumns[i]];
248+
if (targetColumnMapping != undefined && targetColumns.indexOf(targetColumnMapping) != -1) {
249+
continue;
250+
}
251+
252+
try {
253+
cloneColumn(sourceTableName, targetTableName, sourceColumns[i]);
254+
} catch (ex) {
255+
gs.error(
256+
"Upgrade: Exception occurred while creating column '{0}' in table '{1}'.",
257+
sourceColumns[i],
258+
targetTableName
259+
);
260+
return false;
261+
}
262+
}
263+
}
264+
265+
var backupRecordGr = new GlideRecord(BACKUP_TABLE_NAME);
266+
backupRecordGr.addQuery("table_name", sourceTableName);
267+
backupRecordGr.query();
268+
269+
while (backupRecordGr.next()) {
270+
var recordId = backupRecordGr.getValue("record_id");
271+
var modelGr = new GlideRecord(sourceTableName);
272+
modelGr.get(recordId);
273+
274+
if (gs.nil(modelGr.next())) {
275+
continue;
276+
}
277+
278+
var colValues = {};
279+
280+
for (var j = 0; j < sourceColumns.length; j++) {
281+
colValues[sourceColumns[j]] = modelGr.getValue(sourceColumns[j]);
282+
}
283+
284+
// Using GlideRecordClassSwitcher to migrate data from source to the target table
285+
var switcher = new GlideRecordClassSwitcher(modelGr, sourceTableName, false);
286+
var isSwitchingSuccessful = switcher.switchClass(
287+
targetTableName,
288+
sourceTableName + " to " + targetTableName + " migration. "
289+
);
290+
291+
if (gs.nil(isSwitchingSuccessful)) {
292+
continue;
293+
}
294+
295+
modelGr = new GlideRecord(targetTableName);
296+
modelGr.get(recordId);
297+
298+
try {
299+
modelGr.setWorkflow(false);
300+
301+
for (var k = 0; k < sourceColumns.length; k++) {
302+
var fieldName = sourceColumns[k];
303+
304+
// exclude populating column if it is excludeColumns list
305+
if (excludeColumns.indexOf(fieldName) != -1) {
306+
continue;
307+
}
308+
309+
// checking if alternate mapping exists then skip for now
310+
if (!gs.nil(alternateMapping[fieldName])) {
311+
continue;
312+
}
313+
314+
var fieldValue = colValues[fieldName];
315+
modelGr.setValue(fieldName, fieldValue);
316+
}
317+
318+
// setting alternateMapping in different loop to get it prioritize over the existing value
319+
for (alternateMappingKey in alternateMapping) {
320+
try {
321+
modelGr.setValue(alternateMapping[alternateMappingKey], colValues[alternateMappingKey]);
322+
} catch (ex) {
323+
gs.error(
324+
"Upgrade: Exception occurrred while setting column '{0}' in table '{1}' for sys_id '{2}': {3}",
325+
alternateMappingKey,
326+
targetTableName,
327+
modelGr.getUniqueValue(),
328+
ex
329+
);
330+
}
331+
}
332+
333+
modelGr.update();
334+
backupRecordGr.setValue("state", PROCESSED_BACKUP_STATE);
335+
backupRecordGr.update();
336+
} catch (ex) {
337+
gs.error(
338+
"Upgrade: Exception occurred while inserting data in '{0}' with sys_id '{1}': {2}",
339+
targetTableName,
340+
modelGr.getUniquevalue(),
341+
ex
342+
);
343+
}
344+
}
345+
} catch (ex) {
346+
gs.error(
347+
"Upgrade: Exception occurred while migrating data from '{0}' to '{1}': {2}",
348+
sourceTableName,
349+
targetTableName,
350+
ex
351+
);
352+
}
353+
}

0 commit comments

Comments
 (0)