С проблемой загрузки больших 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)
aleksandy
00.00.0000 00:00@Evgeniy_Lyashov, вы тут вроде бы память экономите, а вместо нормальных примитивов обёртки используете. У вас же практически в каждом методе потенциальный NPE.
Обёртки допустимы в сущностях, особенно, когда атрибуты сущности отображаются на nullable-поля в таблице.
Gmugra
00.00.0000 00:00Спасибо за статью, в принципе любопытно.
Но, блин, ваш XMLAttributeReader не пройдет никакое ревью. Это просто плохой код.
Сделайте его AutoCloseable. Тогда его можно будет пихать в try-with-resource и не надо будет думать о методе close()
У attributesToMap потеряны генерики в возвращаемом типе. На это даже IDE повесит стандартный Warning.
Зачем switch? На него, опять же, даже IDE повесит стандартный Warning за отсутствие default. Чем не устраивает простой if?
Прятать исключения - это плохой стиль. Кидайте их "наверх", пусть вызывающий класс думает что с ними делать.
Если пункт 4, то вообще не нужен Logger. Но даже если он вам таки нужен то private static final Logger ...
За return после try-catch, как у вас в getNextPart - отрывают руки. Это незнание основ. (И опять же - если пункт 4 то никакого try-catch просто нет.)
Зачем attributesToMap static? Вот зачем? думаете поможете JVM? Но компилятор, скорее всего, все равно заинлайнит этот метод. Оно только вопросы вызывает и больше никакого толку.
beho1der
и за сколько времени происходит полная загрузка данных? Еще можно сэкономить на времени\памяти если читать из архива сразу,без распаковки.