С проблемой загрузки больших 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. Знаем где какой вариант лучше подойдет :)

Всем спасибо! Комментарии приветствуются ????

Комментарии (6)


  1. beho1der
    00.00.0000 00:00

    и за сколько времени происходит полная загрузка данных? Еще можно сэкономить на времени\памяти если читать из архива сразу,без распаковки.


  1. aleksandy
    00.00.0000 00:00

    @Evgeniy_Lyashov, вы тут вроде бы память экономите, а вместо нормальных примитивов обёртки используете. У вас же практически в каждом методе потенциальный NPE.

    Обёртки допустимы в сущностях, особенно, когда атрибуты сущности отображаются на nullable-поля в таблице.


  1. Yo1
    00.00.0000 00:00

    Spark с этим справился бы в несколько строк и в разы быстрей даже на ноутбуке , за счет распараллеливания.


    1. ris58h
      00.00.0000 00:00

      Ждём статьи с реальными тестами.


    1. Flylink
      00.00.0000 00:00
      +1

      На java тоже можно распараллелить


  1. Gmugra
    00.00.0000 00:00

    Спасибо за статью, в принципе любопытно.

    Но, блин, ваш XMLAttributeReader не пройдет никакое ревью. Это просто плохой код.

    1. Сделайте его AutoCloseable. Тогда его можно будет пихать в try-with-resource и не надо будет думать о методе close()

    2. У attributesToMap потеряны генерики в возвращаемом типе. На это даже IDE повесит стандартный Warning.

    3. Зачем switch? На него, опять же, даже IDE повесит стандартный Warning за отсутствие default. Чем не устраивает простой if?

    4. Прятать исключения - это плохой стиль. Кидайте их "наверх", пусть вызывающий класс думает что с ними делать.

    5. Если пункт 4, то вообще не нужен Logger. Но даже если он вам таки нужен то private static final Logger ...

    6. За return после try-catch, как у вас в getNextPart - отрывают руки. Это незнание основ. (И опять же - если пункт 4 то никакого try-catch просто нет.)

    7. Зачем attributesToMap static? Вот зачем? думаете поможете JVM? Но компилятор, скорее всего, все равно заинлайнит этот метод. Оно только вопросы вызывает и больше никакого толку.