С проблемой загрузки больших XML столкнулся при переходе с КЛАДР и ФИАС на справочники ГАР — Государственный адресный реестр (Федеральная информационная адресная система).
Справочник ГАР содержит более подробную информацию чем предыдущие классификаторы. В том числе информацию по муниципальным делениям. В связи с чем справочник после распаковки занимет около 250 ГБ, что примерно в 3 раза больше чем тот же ФИАС.
Предыдущая загрузка работала на DOM-модели, т.е. весь XML-файл считывался в память. Соответственно при попытке загрузить ГАР таким же способом стали стабильно получать OutOfMemory. А значит настало время менять подход к загрузке)
Немного теории:
DOM (Document Object Model) — это стандартный интерфейс для работы с документами в формате XML (Extensible Markup Language). DOM-модель представляет XML-документ в виде дерева объектов, где каждый элемент и атрибут документа является узлом дерева.
SAX (Simple API for XML) является событийно-ориентированным API для чтения XML-документа. Он предоставляет возможность читать XML-документ последовательно и обрабатывать события, такие как начало и конец элемента, содержимое элемента и т.д.
StAX (Streaming API for XML) также является API для последовательного чтения и записи XML-документов. Он предоставляет потоковый доступ к XML-документу, позволяя читать его и записывать по частям. StAX предоставляет возможность читать и записывать XML-документы в виде потока событий, аналогично SAX, но также предоставляет возможность читать и записывать XML-документы в виде итерируемых наборов событий. StAX позволяет эффективно обрабатывать большие XML-документы и не требует реализации обработчиков событий.
Другими словами:
Загрузка XML-документа с помощью DOM-модели довольно медленная, особенно для больших документов, поскольку требует создания полной структуры DOM в памяти. Однако, DOM-модель позволяет легко и удобно работать с XML-документами, поэтому она широко используется в Java-приложениях.
SAX и StAX позволяет обрабатывать XML-документы любого размера, поскольку он не хранит всю структуру в памяти. Однако, для работы необходимо реализовать обработчики событий.
Одним из главных преимуществ использования StAX является его скорость работы и эффективность. И и отличие от DOM, который загружает весь XML-документ в память перед его обработкой, StAX-парсер обрабатывает XML-документ по одному элементу за раз, что позволяет работать с большими XML-файлами.
Понятно. Останавливаемся на StAX 🙂
Реализуем класс для универсальной загрузки XML. Будем читать данные комфортными порциями и мапить их на произвольные объекты. Класс объекта передаем в загрузчик.
public class XMLAttributeReader { private Logger logger = LoggerFactory.getLogger(XMLAttributeReader.class); private InputStream inputStream; private String attr; private XMLEventReader eventReader; private ObjectMapper mapper; private final Integer RECORDS_COUNT; private void configure() { mapper = new ObjectMapper(); mapper.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true); mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); try { XMLInputFactory factory = XMLInputFactory.newInstance(); eventReader = factory.createXMLEventReader(inputStream); } catch (XMLStreamException e) { logger.error(e.getMessage()); } } public void close(){ try { eventReader.close(); } catch (XMLStreamException e) { e.printStackTrace(); } } public XMLAttributeReader(InputStream inputStream, String attr, Integer RECORDS_COUNT) { this.inputStream = inputStream; this.attr = attr; this.RECORDS_COUNT = RECORDS_COUNT; configure(); } public Boolean hasNext() { return eventReader != null ? eventReader.hasNext() : false; } public <T> List<T> getNextPart(Class<T> valueType) { List<T> valueList = new ArrayList<>(); int count = 0; try { while (eventReader.hasNext() && count < RECORDS_COUNT) { XMLEvent event = eventReader.nextEvent(); switch (event.getEventType()) { case XMLStreamConstants.START_ELEMENT: StartElement startElement = event.asStartElement(); String qName = startElement.getName().getLocalPart(); if (qName.equalsIgnoreCase(attr)) { Map map = attributesToMap(startElement.getAttributes()); T value = mapper.convertValue(map, valueType); valueList.add(value); count++; } break; } } } catch (XMLStreamException e) { logger.error(e.getMessage()); } return valueList; } private static Map attributesToMap(Iterator<Attribute> attributes) { Map<String, String> map = new HashMap<>(); while (attributes.hasNext()) { Attribute attr = attributes.next(); map.put(attr.getName().toString(), attr.getValue()); } return map; } }
Структура справочника состоит из нескольких типов XML-файлов. Для каждой создадим таблицу в БД, опишим сущности. Пример для адресных объектов:
package com.example.XMLToBase.db.entity; import javax.persistence.*; import java.util.Date; //<OBJECT ID="1178934" OBJECTID="948460" OBJECTGUID="b6ea12e7-eb66-46e4-9329-fb3dbfd09827" // CHANGEID="2615278" // NAME="Ветеран квартал 6" // TYPENAME="снт" LEVEL="7" // OPERTYPEID="50" PREVID="1178870" // NEXTID="1909861" UPDATEDATE="2021-05-07" STARTDATE="2016-09-29" ENDDATE="2021-05-07" // ISACTUAL="0" ISACTIVE="0" /><OBJECT ID="1909471" OBJECTID="101148944" // OBJECTGUID="73104935-cc12-4bc7-b2d1-70c431aa7005" CHANGEID="192807935" // NAME="Южный" TYPENAME="пер" LEVEL="8" OPERTYPEID="10" PREVID="0" NEXTID="0" // UPDATEDATE="2021-05-05" STARTDATE="2021-05-05" ENDDATE="2079-06-06" ISACTUAL="1" // ISACTIVE="1" /><OBJECT ID="1909861" OBJECTID="948460" OBJECTGUID="b6ea12e7-eb66-46e4-9329-fb3dbfd09827" // CHANGEID="192832273" NAME="Ветеран квартал 6" TYPENAME="снт" LEVEL="7" OPERTYPEID="30" PREVID="1178934" NEXTID="0" // UPDATEDATE="2021-05-07" STARTDATE="2021-05-07" ENDDATE="2079-06-06" ISACTUAL="1" ISACTIVE="0" /></ADDRESSOBJECTS> @Data @Entity @Table(name = "gar_addressobject") public class GarAddressobject { @Id @Column(name = "id") private Long id; @Column(name = "objectid") private Long objectid; @Column(name = "objectguid") private String objectguid; @Column(name = "changeid") private Long changeid; @Column(name = "name") private String name; @Column(name = "typename") private String typename; @Column(name = "level") private String level; @Column(name = "opertypeid") private Long opertypeid; @Column(name = "previd") private Long previd; @Column(name = "nextid") private Long nextid; @Column(name = "updatedate") private Date updatedate; @Column(name = "startdate") private Date startdate; @Column(name = "enddate") private Date enddate; @Column(name = "isactual") private Long isactual; @Column(name = "isactive") private Long isactive; }
Читаем нашим XML загрузчиком адреса пачками в структуру GarAddressobject и тут же производим сохранение в БД.
private void processAddr(File file){ logger.info("Start loading " + file.getParent() + "/" + file.getName()); try (InputStream is = new FileInputStream(file)) { XMLAttributeReader xmlReader = new XMLAttributeReader(is, "OBJECT", RECORDS_PER_ITERATION); int i = 0; int j = 1; List<GarAddressobject> list; while (xmlReader.hasNext()) { list = xmlReader.getNextPart(GarAddressobject.class); garAddressobjectRepository.saveAll(list); i += Math.min(RECORDS_PER_ITERATION, list.size()); if ((i / RECORDS_PER_ITERATION) != j){ j = i / RECORDS_PER_ITERATION; logger.info("saved records: " + i); } list.clear(); } logger.info("saved records: " + i); xmlReader.close(); } catch (IOException e) { e.printStackTrace(); } }
Для файлов домов, муниципальных образований и пр. алгоритм такой же, полный код загрузчика по ссылке
В итоге имеем:
-
Регламентная загрузка ГАР перестала падать изза нехватки памяти
-
Можно управлять кол-вом строк, которые за раз вычитывает XML-загрузчик
-
Сам загрузчик довольно универсальный, его можно переиспользовать в других задачах
-
Понимаем отличия в подходах DOM и SAX. Знаем где какой вариант лучше подойдет 🙂
Всем спасибо! Комментарии приветствуются ?
ссылка на оригинал статьи https://habr.com/ru/post/724324/
Добавить комментарий