I am importing XML data to a MySql database using LINQ to Entities. The data represents which students are in which classes, and looks like...
<studentClassesData>
<studentClassEntry>
<upn>W432980573452</upn>
<className>8bcDn4</className>
</studentClassEntry>
<studentClassEntry>
<upn>W234525211334</upn>
<className>8bcPe1</className>
</studentClassEntry>
<!-- etc... -->
</studentClassesData>
Each entry represents one student in one class. I know it would be better if the format were hierarchical (so each entry gives us all the students in a class), but the XML is generated externally so we have to use it as is.
Receiving this information are two entities, Class
and Student
, which are linked in a many-to-many relationship (e.g. Class.Students
). All the students and classes have already been imported to the database.
Note that Student.Upn
and Class.LongName
are unique keys which correspond to the values given in the XML. But the primary keys are Student.Id
and Class.Id
.
Here's my current code, with verbose comments added for explanation.
public static string ProcessStudentClasses(string path) {
var sb = new StringBuilder();
using (var ctx = new Ctx()) {
// Create a dictionary of all classes, to be accessed by their LongName
// (attach the classes, but don't bother loading their properties)
var classes = ctx.Classes.Select(o => new { o.LongName, o.Id })
.ToDictionary(o => o.LongName, o => new Class { Id = o.Id });
foreach (var cls in classes.Values) ctx.Classes.Attach(cls);
// Similarly for students
var students = ctx.Students.Select(o => new { o.Upn, o.Id })
.ToDictionary(o => o.Upn, o => new Student { Id = o.Id });
foreach (var stu in students.Values) ctx.Students.Attach(stu);
// Create a lookup of which classes already have which students
var studentsInClasses =
ctx.Classes.SelectMany(c => c.Students,
(c, s) => new { ClassId = c.Id, StudentId = s.Id }
).ToLookup(o => o.ClassId, o => o.StudentId);
using (var reader = XmlReader.Create(path)) {
var nodeName = String.Empty;
var upn = String.Empty;
var className = String.Empty;
Student thisStudent = null;
Class thisClass = null;
while (reader.Read()) {
switch (reader.NodeType) {
case XmlNodeType.Element:
// We're reading an XML element; note which element it is
nodeName = reader.Name;
break;
case XmlNodeType.Text:
// We're reading text (what's between the <nodeName> and </nodeName> tags)
var trimmed = reader.Value.Trim();
if (trimmed.Length == 0) continue;
switch (nodeName) {
case "upn":
upn = trimmed;
students.TryGetValue(upn, out thisStudent); // set 'thisStudent'
break;
case "className":
className = trimmed;
classes.TryGetValue(className, out thisClass); // set 'thisClass'
break;
}
break;
case XmlNodeType.EndElement:
if (reader.Name != "studentClassEntry") continue;
// We've got to the end of a studentClassEntry
// If the student wasn't set...
if (thisStudent == null) {
sb.AppendFormat("Skipped: {0} - {1} (unknown UPN)<br />", upn, className);
// If the class wasn't set...
} else if (thisClass == null) {
sb.AppendFormat("Skipped: {0} - {1} (unknown class)<br />", upn, className);
// If the student's already in the class...
// (Note, this only checks against the original list of students in classes;
// an error will rightly be thrown if the imported XML contains duplicates)
} else if (studentsInClasses[thisClass.Id].Contains(thisStudent.Id)) {
sb.AppendFormat("Skipped: {0} - {1} (already exists)<br />", upn, className);
// Otherwise (we're good to add the student to the class)...
} else {
thisClass.Students.Add(thisStudent);
sb.AppendFormat("Imported: {0} - {1}<br />", upn, className);
}
// Reset everything, ready for the next entry
thisStudent = null;
thisClass = null;
upn = String.Empty;
className = String.Empty;
break;
}
}
}
ctx.SaveChanges();
}
return sb.ToString();
}
It takes 13.1 sec to process just over 21,000 student-class entries.
I'd very much appreciate any suggestions.