Breaking News
القائمة

كيفية بناء نظام استيراد ملفات Excel قوي في Spring Boot باستخدام Apache POI

كيفية بناء نظام استيراد ملفات Excel قوي في Spring Boot باستخدام Apache POI
100%

يواجه المطورون الذين يبنون لوحات تحكم إدارية تحدياً متكرراً يتمثل في استيراد ملفات Excel التي ينشئها المستخدمون إلى قواعد البيانات دون التسبب في انهيار الخادم أو تلف البيانات. تتطلب معالجة ملفات .xlsx باستخدام إطار Spring Boot التعامل مع طلبات الرفع المتعددة، وتحليل الخلايا عبر مكتبة Apache POI، والتحقق من صحة الصفوف، وتنفيذ عمليات الإدراج على دفعات. وتُعد إدارة مدرسة ترفع قائمة بأسماء الطلاب ودرجاتهم حالة نموذجية: معظم الصفوف تكون سليمة، بينما يحتوي بعضها على بريد إلكتروني خاطئ أو درجة غير صالحة، ويجب على النظام تحديد الخلايا التي تحتاج إلى تعديل بدقة.

Advertisement

يقرأ المثال التالي خليتين من صف القائمة باستخدام تنسيق البيانات (DataFormatter) ويتحقق من صحة الصف عبر أداة Bean Validation قبل حفظ بيانات الطلاب الصالحة في قاعدة البيانات. بعد ذلك، يرفع أمر curl القائمة إلى نقطة نهاية الاستيراد.

DataFormatter formatter = new DataFormatter();
String roll = formatter.formatCellValue(row.getCell(0));     // "101" (numeric cell)
String date = formatter.formatCellValue(row.getCell(5));     // "6/15/24" (date cell)

var violations = validator.validate(studentRow);             // email "chloe.school.com" -> "must be a well-formed email address"
repository.saveAll(students);                                // inserts in JDBC batches of 50
curl -F "file=@samples/students.xlsx" http://localhost:8080/students/import
# {"totalRows":8,"imported":4,"failed":4,"errors":[{"row":5,"column":"Email","message":"must be a well-formed email address"}, ...]}

يُلاحظ أن أداة تنسيق البيانات (DataFormatter) تُرجع رقم التسجيل الرقمي كنص "101"، وتستجيب نقطة النهاية بتقرير JSON يحدد رقم الصف والعمود لكل خطأ. نستعرض أدناه البنية الكاملة لبناء نقطة النهاية هذه، بما يشمل قراءة الخلايا، وتعيين العناوين، والتحقق من التكرار، وبث الملفات الكبيرة.

كيف تعمل عملية استيراد Excel في Spring Boot؟

يُعد ملف .xlsx أرشيفاً مضغوطاً (zip) لملفات XML بتنسيق Office Open XML. تُمثل كل ورقة عمل ملف XML مستقلاً، بينما يحتوي ملفان آخران على الجدول المشترك للسلاسل النصية المتكررة والأنماط التي تحدد كيفية عرض Excel للأرقام والتواريخ. وتُعد مكتبة Apache POI الأداة البرمجية في Java المسؤولة عن قراءة وكتابة هذه الملفات. تمر نقطة نهاية الاستيراد بأربع خطوات: التحقق من الملف، وتعيين العناوين، والتحقق من الصفوف، والإدراج في قاعدة البيانات.

توفر مكتبة Apache POI طريقتين لقراءة ورقة العمل. تبني الطريقة القياسية، عبر واجهة XSSFWorkbook، كائناً لكل صف وخلية في الذاكرة. في المقابل، تحلل واجهة XSSF event API ملف XML الخاص بالورقة باستخدام محلل SAX (الذي يقرأ عنصراً واحداً من XML في كل مرة) وتحتفظ بالصف الحالي فقط في الذاكرة.

الميزة واجهة XSSFWorkbook (نموذج المستخدم) واجهة XSSF event API (محلل SAX)
الذاكرة المصنف بالكامل ككائنات الصف الحالي بالإضافة إلى السلاسل المشتركة
الكود حلقة تكرار عبر Sheet و Row و Cell استدعاء لكل خلية (SheetContentsHandler)
الوصول العشوائي نعم، sheet.getRow(500) لا، تصل الصفوف بترتيب الملف
المعادلات يمكن تقييمها يقرأ النتيجة المخزنة مؤقتاً
مناسب لـ أوراق العمل التي تحتوي على بضعة آلاف من الصفوف الملفات الكبيرة، الذاكرة المحدودة

تُنتج كلتا الطريقتين النص ذاته لكل خلية، مما يضمن بقاء منطق التحقق والحفظ متطابقاً بغض النظر عن طريقة التحليل المستخدمة.

تبعيات Maven وإعداد الكيانات

لا يدير إطار Spring Boot إصدار مكتبة Apache POI تلقائياً، مما يتطلب تكويناً يدوياً. تتعامل وحدة poi-ooxml مع ملفات .xlsx وتسحب الوحدة الأساسية poi تلقائياً.


  org.springframework.boot
  spring-boot-starter-webmvc


  org.springframework.boot
  spring-boot-starter-data-jpa


  org.springframework.boot
  spring-boot-starter-validation


  org.apache.poi
  poi-ooxml
  5.5.1


  com.h2database
  h2
  runtime

يُعد كيان قاعدة البيانات فئة JPA قياسية. يفرض حقل rollNumber قيداً فريداً، ويتم إنشاء المعرف (ID) من تسلسل، وهو أمر بالغ الأهمية لتمكين عمليات الإدراج المجمعة لاحقاً.

@Entity
public class Student {

  @Id
  @GeneratedValue(strategy = GenerationType.SEQUENCE)
  @SequenceGenerator(sequenceName = "student_seq", allocationSize = 50)
  private Long id;

  @Column(unique = true, nullable = false)
  private String rollNumber;

  private String name;
  private String email;
  private String className;
  private Integer marks;
  private LocalDate admissionDate;
}

بدلاً من التحقق من الكيان مباشرة، يحتفظ سجل StudentRow بصف واحد محول إلى جانب رقم صف Excel الخاص به، حاملاً قيود أداة Bean Validation.

public record StudentRow(
    int rowNumber,
    @NotBlank @Pattern(regexp = "\\d{1,6}", message = "must be a number with up to 6 digits") String rollNumber,
    @NotBlank @Size(max = 100) String name,
    @NotBlank @Email String email,
    @NotBlank String className,
    @NotNull @Min(0) @Max(100) Integer marks,
    @NotNull @PastOrPresent LocalDate admissionDate) {
}

يربط تعداد (Enum) كل عنوان باسم حقل محدد. تُبلغ أداة التحقق عن الأخطاء باسم الحقل، ويترجم التعداد ذلك مرة أخرى إلى نص العنوان الأصلي لتقرير المستخدم النهائي.

public enum StudentColumn {
  ROLL_NUMBER("Roll No", "rollNumber"),
  NAME("Name", "name"),
  EMAIL("Email", "email"),
  CLASS_NAME("Class", "className"),
  MARKS("Marks", "marks"),
  ADMISSION_DATE("Admission Date", "admissionDate");
  // constructor, header(), field() and headerOf(field) omitted
}

رفع الملفات باستخدام MultipartFile

في بيئة Spring، يمثل كائن MultipartFile ملفاً تم رفعه عبر طلب multipart/form-data. تتحقق وحدة التحكم من امتداد الملف وتنقل الملف المرفوع إلى ملف مؤقت على القرص.

@PostMapping(value = "/students/import", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ImportReport importStudents(@RequestParam("file") MultipartFile file,
    @RequestParam(defaultValue = "false") boolean streaming,
    @RequestParam(defaultValue = "false") boolean allOrNothing) {

  String fileName = file.getOriginalFilename();
  if (file.isEmpty() || fileName == null || !fileName.toLowerCase().endsWith(".xlsx")) {
    throw new InvalidFileException("Upload a non-empty .xlsx file");
  }
  SheetReader reader = streaming ? streamingReader : workbookReader;
  Path upload = null;
  try {
    upload = Files.createTempFile("students-", ".xlsx");
    file.transferTo(upload);                     // POI reads a file part by part
    return importService.importStudents(upload, reader, allOrNothing);
  } catch (IOException | UnsupportedFileFormatException e) {
    throw new InvalidFileException("Not a readable .xlsx file");
  } finally {
    deleteQuietly(upload);
  }
}

تتطلب مكتبة Apache POI ملفاً فعلياً بدلاً من تدفق بيانات (InputStream) لتجنب تخزين الملف بالكامل في الذاكرة. إذا لم يكن الملف أرشيفاً مضغوطاً صالحاً، تطرح POI استثناءً، والذي تحوله وحدة التحكم إلى استجابة 400 باستخدام معيار تفاصيل المشكلة RFC 9457.

@ExceptionHandler(InvalidFileException.class)
ProblemDetail invalidFile(InvalidFileException e) {
  return ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, e.getMessage());
}

قراءة الخلايا باستخدام DataFormatter

تمتلك كل خلية في Excel نوعاً محدداً: نصي، أو رقمي، أو منطقي، أو معادلة، أو فارغ. تؤدي محاولة قراءة خلية باستخدام دالة جلب خاطئة إلى طرح استثناء، وهو السبب الأكثر شيوعاً لأخطاء الاستيراد.

String text = roll.getStringCellValue();                        // IllegalStateException: Cannot get a STRING value from a NUMERIC cell
double number = roll.getNumericCellValue();                     // 101.0

DataFormatter formatter = new DataFormatter();
String rollText = formatter.formatCellValue(roll);              // "101"
String marksText = formatter.formatCellValue(marks);            // "85.5"
String dateText = formatter.formatCellValue(date);              // "6/15/24" (built-in format m/d/yy)
String formulaText = formatter.formatCellValue(formula);        // "B1*2" (the formula, not the result)
String result = formatter.formatCellValue(formula, evaluator);  // "171"
String missing = formatter.formatCellValue(row.getCell(9));     // "" (missing cell)

تُرجع فئة تنسيق البيانات (DataFormatter) النص الدقيق الذي يعرضه Excel للخلية، بغض النظر عن نوعها الأساسي. تتطلب التواريخ معالجة إضافية لأن النص المعروض يعتمد على تنسيق المستخدم. يضمن توسيع فئة DataFormatter إرجاع التواريخ باستمرار بتنسيق ISO.

public class IsoDateFormatter extends DataFormatter {

  @Override
  public String formatCellValue(Cell cell) {
    if (cell != null && cell.getCellType() == CellType.NUMERIC && DateUtil.isCellDateFormatted(cell)) {
      return cell.getLocalDateTimeCellValue().toLocalDate().toString();   // "2024-06-15"
    }
    return super.formatCellValue(cell).trim();
  }

  @Override
  public String formatRawCellContents(double value, int formatIndex, String formatString,
      boolean use1904Windowing) {
    if (DateUtil.isADateFormat(formatIndex, formatString) && DateUtil.isValidExcelDate(value)) {
      return DateUtil.getLocalDateTime(value, use1904Windowing).toLocalDate().toString();
    }
    return super.formatRawCellContents(value, formatIndex, formatString, use1904Windowing);
  }
}

قراءة الصفوف وتعيين العناوين بالاسم

يتخطى النظام الصفوف التي لم تُكتب مطلقاً أو التي تحتوي على سلاسل فارغة فقط. يتم ترقيم الصفوف بدءاً من 1، ليتطابق مع واجهة Excel الأصلية.

XSSFWorkbook workbook = new XSSFWorkbook(pkg);       // pkg = OPCPackage.open(file, PackageAccess.READ)
Sheet sheet = workbook.getSheetAt(0);
for (int r = sheet.getFirstRowNum(); r <= sheet.getLastRowNum(); r++) {
  Row row = sheet.getRow(r);
  if (row == null) {
    continue;                                        // row never written
  }
  List values = new ArrayList<>();
  for (int c = 0; c < row.getLastCellNum(); c++) {
    values.add(formatter.formatCellValue(row.getCell(c)));   // "" for a missing cell
  }
  if (!SheetReader.isBlank(values)) {
    rows.accept(new SheetRow(r + 1, values));        // Excel row number
  }
}

يعمل أول صف غير فارغ كعنوان. يعين النظام كل عمود بنص العنوان الخاص به، متجاهلاً حالة الأحرف والمسافات الزائدة. يمنع هذا النهج تلف البيانات إذا قام المستخدم بإدراج عمود أو نقله قبل الرفع.

Map positions = new HashMap<>();
for (int i = 0; i < row.values().size(); i++) {
  positions.put(row.value(i).trim().toLowerCase(), i);       // "roll no" -> 0, "class" -> 2
}
for (StudentColumn column : StudentColumn.values()) {
  Integer index = positions.get(column.header().toLowerCase());
  if (index == null) {
    missing.add(column.header());
  } else {
    columns.put(column, index);
  }
}
if (!missing.isEmpty()) {
  throw new InvalidFileException("Missing columns: " + String.join(", ", missing));
}

إذا كان هناك عمود مطلوب مفقود، يتوقف الاستيراد فوراً ويُرجع استجابة 400 Bad Request تفصل العناوين المفقودة بدقة.

{"detail":"Missing columns: Email","instance":"/students/import","status":400,"title":"Bad Request"}

التحقق من كل صف والإبلاغ عن الأخطاء

يخضع كل صف لعملية تحويل وتحقق. يتم تحويل القيم النصية للدرجات والتواريخ إلى أنواع Java المقابلة لها، ويتم تسجيل أي فشل مقابل عنوان العمود.

try {
  marks = Integer.valueOf(marksText);                  // "absent" -> NumberFormatException
} catch (NumberFormatException e) {
  badColumns.add(addError(row, StudentColumn.MARKS, "must be a whole number"));
}

for (ConstraintViolation violation : validator.validate(student)) {
  String column = StudentColumn.headerOf(violation.getPropertyPath().toString());   // "email" -> "Email"
  if (!badColumns.contains(column)) {
    errors.add(new RowError(row.rowNumber(), column, violation.getMessage()));
  }
}

التقرير النهائي عبارة عن كائن JSON يحتوي على إجمالي عدد الصفوف، والواردات الناجحة، والصفوف الفاشلة، ومصفوفة مفصلة بالأخطاء. يؤدي رفع قائمة نموذجية إلى تشغيل عملية التحقق هذه.

curl -F "file=@samples/students.xlsx" http://localhost:8080/students/import
{"totalRows":8,"imported":4,"failed":4,"errors":[
  {"row":5,"column":"Email","message":"must be a well-formed email address"},
  {"row":6,"column":"Marks","message":"must be less than or equal to 100"},
  {"row":6,"column":"Admission Date","message":"must be a date such as 2024-06-15"},
  {"row":7,"column":"Roll No","message":"duplicate of row 3"},
  {"row":8,"column":"Name","message":"must not be blank"},
  {"row":8,"column":"Marks","message":"must be a whole number"}]}

التعامل مع أرقام التسجيل المتكررة

يمكن أن توجد إدخالات متكررة داخل الملف المرفوع نفسه أو تتعارض مع سجلات قاعدة البيانات الحالية. تتتبع خريطة الذاكرة التكرارات داخل الملف الحالي أثناء التحليل.

Integer firstRow = seenRollNumbers.putIfAbsent(student.rollNumber(), row.rowNumber());
if (firstRow != null) {                               // row 7: "102" was seen in row 3
  errors.add(new RowError(row.rowNumber(), "Roll No", "duplicate of row " + firstRow));
}

بالنسبة لتعارضات قاعدة البيانات، يحدد استعلام واحد لكل دفعة مكونة من 500 صف أرقام التسجيل التي تم حفظها بالفعل.

@Query("select s.rollNumber from Student s where s.rollNumber in :rollNumbers")
List findExistingRollNumbers(Collection rollNumbers);

إذا تم رفع الملف نفسه مرتين، يرفض النظام الصفوف المحفوظة مسبقاً ويضيف رسالة "موجود بالفعل" إلى تقرير الأخطاء.

{"totalRows":8,"imported":0,"failed":8,"errors":[
  {"row":2,"column":"Roll No","message":"already exists"},
  {"row":3,"column":"Roll No","message":"already exists"},
  {"row":5,"column":"Email","message":"must be a well-formed email address"},
  ...
  {"row":10,"column":"Roll No","message":"already exists"}]}

حفظ الصفوف على دفعات

ترسل عمليات JPA القياسية عبارة INSERT واحدة لكل كيان. يتيح تمكين تجميع JDBC لإطار Hibernate تجميع ما يصل إلى 50 عملية إدراج في استدعاء واحد لقاعدة البيانات، مما يقلل بشكل كبير من عبء الشبكة.

spring.jpa.properties.hibernate.jdbc.batch_size=50
spring.jpa.properties.hibernate.order_inserts=true

تعالج الخدمة الصفوف الصالحة في دفعات من 500. يؤدي استدعاء flush() إلى تنفيذ عمليات الإدراج المجمعة، بينما يفصل clear() الكيانات لمنع تضخم الذاكرة.

repository.saveAll(students);   // persist() per student, sent in JDBC batches of 50
entityManager.flush();          // run the batched inserts now
entityManager.clear();          // free the saved entities before the next chunk

عند استيراد 1,200 صف صالح، تؤكد إحصائيات Hibernate تنفيذ عمليات الإدراج في 24 استدعاءً فقط لقاعدة البيانات بدلاً من 1,200 عبارة فردية.

DEBUG org.hibernate.session.metrics : HHH000401: Logging session metrics:
  5891555 ns preparing 30 JDBC statements
  17971287 ns executing 27 JDBC statements
  156534667 ns executing 24 JDBC batches
  432371392 ns executing 4 flushes (flushing a total of 1200 entities and 0 collections)

قراءة الملفات الكبيرة باستخدام SAX Event API

بينما تكفي واجهة XSSFWorkbook للقوائم الصغيرة، تتطلب مجموعات البيانات الكبيرة واجهة XSSF event API. يقرأ هذا النهج التدفقي ملف XML كتدفق مستمر، مما يؤدي إلى تشغيل استدعاءات لكل خلية دون تحميل المستند بأكمله في الذاكرة.

XSSFReader reader = new XSSFReader(pkg);
ReadOnlySharedStringsTable strings = new ReadOnlySharedStringsTable(pkg);
try (InputStream sheet = reader.getSheetIterator().next()) {        // first sheet
  XMLReader parser = XMLHelper.newXMLReader();
  parser.setContentHandler(new XSSFSheetXMLHandler(
      reader.getStylesTable(), strings, new RowCollector(rows), new IsoDateFormatter(), false));
  parser.parse(new InputSource(sheet));
}
public void startRow(int rowNum) {
  values = new ArrayList<>();
}

public void cell(String cellReference, String formattedValue, XSSFComment comment) {
  int column = new CellReference(cellReference).getCol();   // "C5" -> 2
  while (values.size() < column) {
    values.add("");                                         // empty cells are not reported
  }
  values.add(formattedValue == null ? "" : formattedValue.trim());
}

public void endRow(int rowNum) {
  if (!SheetReader.isBlank(values)) {
    rows.accept(new SheetRow(rowNum + 1, values));
  }
}

يكشف اختبار ملف بحجم 3,007 كيلوبايت يحتوي على 100,000 صف عن اختلاف هائل في استهلاك الذاكرة بين طريقتي التحليل.

القارئ الحزمة المفتوحة من أصغر -Xmx نجح
واجهة XSSF event API (محلل SAX) ملف 16 ميجابايت (الأصغر الذي جربناه)
واجهة XSSF event API (محلل SAX) تدفق بيانات (InputStream) 96 ميجابايت
واجهة XSSFWorkbook ملف 768 ميجابايت (فشل 512 ميجابايت)
واجهة XSSFWorkbook تدفق بيانات (InputStream) 768 ميجابايت (فشل 512 ميجابايت)

لا يحقق قارئ SAX أقصى كفاءة للذاكرة إلا عند فتح الحزمة مباشرة من ملف. يجبر استخدام تدفق بيانات (InputStream) مكتبة POI على فك ضغط كل جزء في مصفوفات بايت أولاً، مما يلغي فوائد الذاكرة.

الحد من حجم الرفع

يرفض إطار Spring Boot عمليات الرفع التي تتجاوز 1 ميجابايت لكل ملف افتراضياً. لاستيعاب ملف بحجم 3 ميجابايت يحتوي على 100,000 صف، يجب تعديل خصائص التطبيق.

spring.servlet.multipart.max-file-size=5MB
spring.servlet.multipart.max-request-size=6MB

إذا حاول مستخدم رفع ملف يتجاوز هذه الحدود، يطرح Spring استثناء MaxUploadSizeExceededException. يعترض معالج استثناءات مخصص هذا الخطأ ويُرجع استجابة 413 Content Too Large واضحة.

@ExceptionHandler(MaxUploadSizeExceededException.class)
ProblemDetail tooLarge(MaxUploadSizeExceededException e) {
  return ProblemDetail.forStatusAndDetail(HttpStatus.CONTENT_TOO_LARGE,
      "The file is larger than the 5MB upload limit");
}
curl -F "[email protected]" http://localhost:8080/students/import
# HTTP 413
# {"detail":"The file is larger than the 5MB upload limit","instance":"/students/import","status":413,"title":"Content Too Large"}

اختبار الاستيراد باستخدام MockMvc

تبني اختبارات التكامل ملف Excel بالكامل في الذاكرة باستخدام POI، مما يلغي الحاجة إلى تخزين الملفات الثنائية في دليل موارد الاختبار.

rows.add(new Object[]{101, "Anna", "7A", "[email protected]", 85, LocalDate.of(2024, 6, 15)});

switch (values) {
  case Number n -> cell.setCellValue(n.doubleValue());   // numeric cell
  case LocalDate d -> {
    cell.setCellValue(d);
    cell.setCellStyle(dateStyle);                         // data format 14, m/d/yy
  }
  default -> cell.setCellValue(values.toString());    // text cell
}

ByteArrayOutputStream out = new ByteArrayOutputStream();
workbook.write(out);
return out.toByteArray();                                 // the .xlsx bytes for the upload

يرسل إطار MockMvc هذه البايتات كطلب متعدد الأجزاء، للتحقق من كل من استجابة JSON وحالة قاعدة البيانات الناتجة.

@ParameterizedTest
@ValueSource(booleans = {false, true})
void importsValidRowsAndReportsRowErrors(boolean streaming) throws Exception {
  byte[] roster = TestWorkbooks.xlsx(TestWorkbooks.roster());

  mockMvc.perform(multipart("/students/import")
          .file(new MockMultipartFile("file", "class-7.xlsx", XLSX, roster))   // file(roster) in the project
          .param("streaming", String.valueOf(streaming)))
      .andExpect(status().isOk())
      .andExpect(content().json(ROSTER_REPORT, JsonCompareMode.STRICT));

  List saved = repository.findAllByOrderByRollNumber();
  assertThat(saved).extracting(Student::getRollNumber).containsExactly("101", "102", "106", "107");
  Student anna = saved.getFirst();
  assertThat(anna.getMarks()).isEqualTo(85);
  assertThat(anna.getAdmissionDate()).isEqualTo(LocalDate.of(2024, 6, 15));
}

مقارنة CSV بـ Excel ومعالجة الخلايا الرقمية

يعتمد الاختيار بين CSV و Excel كلياً على سير عمل المستخدم النهائي. يُعد CSV أسرع في التحليل ولا يتطلب مكتبات خارجية، ولكنه يزيل التنسيق والأصفار البادئة.

الميزة ملفات CSV ملفات Excel.xlsx
التنسيق نص عادي، مفصول بفواصل أرشيف مضغوط لملفات XML
المكتبة لا يوجد، أو OpenCSV مكتبة Apache POI
التواريخ والأرقام نص فقط، يعتمد التنسيق على من حفظه خلايا مكتوبة بتنسيقات عرض
أوراق عمل متعددة لا نعم
الأصفار البادئة، مثل "007" تُفقد عند فتح الملف وحفظه في Excel تُحفظ في خلية نصية
الذاكرة للملفات الكبيرة سطراً بسطر تحتاج إلى واجهة event API

عند معالجة ملفات Excel، تتطلب قراءة خلية رقمية كسلسلة نصية معالجة محددة. تم استبدال الطريقة المهملة لتغيير نوع الخلية قبل القراءة بفئة DataFormatter.

String rollText = new DataFormatter().formatCellValue(roll);   // "101"

استيراد الكل أو الرفض التام و Spring Batch

تتطلب بعض متطلبات العمل رفض ملف بأكمله إذا فشل صف واحد فقط في التحقق. من خلال تمرير علامة allOrNothing، يمكن للخدمة تحديد المعاملة النشطة للتراجع.

if (allOrNothing && !run.errors.isEmpty()) {
  TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
  imported = 0;                               // the roster: imported 0, failed 4, no rows saved
}

بالنسبة لعمليات الاستيراد الضخمة والمجدولة حيث لا ينتظر المستخدمون استجابة HTTP فورية، يُعد إطار Spring Batch الخيار المعماري الأفضل، حيث يوفر إمكانات إعادة التشغيل المدمجة وحدود التخطي.

المفاضلة في كفاءة الذاكرة

يحدد القرار المعماري بين واجهة XSSFWorkbook وواجهة SAX event API قابلية التوسع للتطبيق بأكمله. تكشف بيانات الأداء عن نقطة ضعف حرجة: ملف Excel بحجم 3 ميجابايت فقط يحتوي على 100,000 صف يجبر XSSFWorkbook على استهلاك 768 ميجابايت من مساحة الذاكرة. في بيئة الحاويات ذات حدود الذاكرة الصارمة، ستؤدي عمليات الرفع المتزامنة بهذا الحجم إلى حدوث أخطاء فورية في نفاد الذاكرة وانهيار النظام.

من خلال الجمع بين حد رفع صارم يبلغ 5 ميجابايت وواجهة SAX event API، ينخفض استهلاك الذاكرة إلى 16 ميجابايت فقط لكل ملف. يضمن هذا التكوين بقاء التطبيق مستقراً تحت الضغط، ومعالجة مجموعات البيانات الضخمة بالتسلسل دون تخزين بنية XML بالكامل. يجب على المطورين إعطاء الأولوية لواجهة event API لأي نقطة نهاية استيراد عامة، مع الاحتفاظ بنموذج المستخدم الأبسط حصرياً للأدوات الداخلية ذات الحجم المنخفض والخاضعة لرقابة صارمة.

هل أعجبك هذا المقال؟
Advertisement

اقرأ المزيد

عمليات البحث الشائعة