Порядок выполнения Liquibase файлов набора изменений при использовании includeAll с classpath*:

Я использую liquibase (3.1.1) в среде Spring (3.2.x) и загружаю наборы изменений через тег inlcudeAll в мастер-файле. Там я использую "classpath*:/package/to/changesets" в качестве пути.

    <?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
         http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">

    <includeAll path="classpath*:/package/to/changesets"/>...

Я использую стратегию именования, например «nnn_changesetname.xml», чтобы сохранить порядок. Но когда я просматриваю таблицу наборов изменений, этот порядок по именам файлов не сохраняется. Это работает только в том случае, если файлы набора изменений содержатся в каталоге, а не в пути к классам?

Обновить

Привет, я обнаружил, что предложенного ниже решения недостаточно. Я думаю, что это связано с реализацией того, как liquibase разрешает атрибут includeAll. В моем случае он сначала разрешает все «папки», а затем просматривает каждую папку в поисках XML-файлов набора изменений. Это нарушит порядок файлов xml во всех местоположениях classpath*:/changes, потому что теперь есть несколько папок «changes» в разных местах. В таком случае я бы заподозрил слияние всего содержимого этих «виртуальных» папок пути к классам и загрузку всех ресурсов в одном перечислении. Или мы могли бы позволить некоторому шаблону ресурсов в теге inlcudeAll, например resources="classpath*:/changes/*.xml", напрямую выбирать все необходимые файлы (попробовали с атрибутом path, но не сработало, потому что он проверяет наличие папка)?

Обновить

Я сделал хак, чтобы проверить, сохраняется ли порядок в возвращаемом перечислении с ответом снизу. Для этого я проверил данное имя пакета, и если оно соответствует моему шаблону, я добавил к нему дополнительный «*.xml». С этим расширением я получаю все изменения по мере необходимости.

@Override
public Enumeration<URL> getResources(String packageName)
  throws IOException {
  if(packageName.equals("classpath*:/plugin/liquibase/changes/")) {
    packageName = packageName + "*.xml";
  }
  List<URL> resources = Collections.list(super.getResources(packageName));
  Collections.sort(resources, new Comparator<URL>() {

    @Override
    public int compare(URL url1, URL url2) {
      String path1 = FilenameUtils.getName(url1.getPath());
      String path2 = FilenameUtils.getName(url2.getPath());
      return String.CASE_INSENSITIVE_ORDER.compare(path1, path2);
    }

  });
  logger.info("Found resources: {}", resources);
  return Collections.enumeration(resources);
}};

В журнале теперь я вижу, что ресурсы имеют правильный порядок. Но когда я смотрю в таблицу DATABASECHANGELOCK, она не отражает порядок, который у меня был в перечислении. Так что кажется, что эти значения переопределяются где-то еще.

Обновить

Далее проанализировал код и обнаружил, что класс liquibase.parser.core.xml.XMLChangeLogSAXHandler производит переупорядочивание возвращаемого перечисления. Так что мои изменения не будут иметь никакого эффекта. Я не думаю, что смогу взломать и этот класс.


person Alex    schedule 07.04.2014    source источник
comment
Ожидаете ли вы файлы, упорядоченные по имени, независимо от того, в каком каталоге они находятся? Или по абсолютному пути к файлу, включая каталоги?   -  person Nathan Voxland    schedule 09.04.2014
comment
Я думаю, проблема в том, что когда вы пишете что-то вроде classpath*:/liquibase/changes, вы предполагаете, что это один и тот же каталог для всех наборов изменений, но это не так, потому что все эти виртуальные папки пути к классам находятся в разных местах jar. Значит виртуальные они в одной папке, физические их нет.   -  person Alex    schedule 09.04.2014
comment
Вы можете заменить синтаксический анализатор XML, работающий через систему расширений, но это потребует больших усилий. Знаете ли вы, какие изменения необходимо будет внести? Отправьте запрос на включение, и я могу включить его.   -  person Nathan Voxland    schedule 12.04.2014
comment
Насколько я видел код парсера XML, было бы разумнее добавить новый атрибут в тег includeAll, например, resourcePattern, который используется вместо атрибута пути. Но я думаю, что теперь это больше не должно быть частью этого вопроса. Я отправлю вам письмо с моими мыслями.   -  person Alex    schedule 12.04.2014


Ответы (2)


Вы правы, Liquibase полагается на базовую логику «списка файлов», которая упорядочивает файлы в алфавитном порядке через файловую систему, но, по-видимому, не через пути к классам.

Я создал https://liquibase.jira.com/browse/CORE-1843 для отслеживания исправление.

На данный момент, если вы настроите Spring с подклассом liquibase.integration.spring.SpringLiquibase, который переопределяет getResources(String packageName) с помощью метода, который сортирует возвращенное перечисление, которое должно решить проблему для вас.

person Nathan Voxland    schedule 07.04.2014
comment
Спасибо, я посмотрел на класс, но мне кажется, что расширения недостаточно, потому что упомянутый метод находится во внутреннем классе с именем SpringResourceOpener. Я постараюсь расширить оба, чтобы добиться ваших исправлений. - person Alex; 09.04.2014

Итак, после некоторого размышления и одной ночи сна я придумал следующий хак, чтобы гарантировать порядок загруженных файлов журнала изменений с помощью шаблона classpath classpath*:/my/path/to/changelog/*.xml . Идея состоит в том, чтобы создать основной файл журнала изменений на лету с помощью манипуляций с dom, когда liquibase запрашивает его.

Это работает только для основного файла журнала изменений. Следующее обязательное условие:

  • Шаблон можно использовать только для основного файла журнала изменений.
  • Я использую пустой основной файл журнала изменений в качестве шаблона
  • Все остальные файлы журнала изменений должны использовать обычный разрешенный механизм загрузки.
  • Работает только в среде Spring

Сначала мне пришлось расширить/переписать liquibase.integration.spring.SpringLiquibase с помощью моей реализации.

public class MySpringLiquibase extends SpringLiquibase {

  private static final Logger logger = LoggerFactory.getLogger(MySpringLiquibase.class);

  private ApplicationContext context;

  private String changeLogLocationPattern;

  private List<String> changeLogLocations;

  @Autowired
  public void setContext(ApplicationContext context) {
    this.context = context;
  }

  /**
   * Location pattern to search for changelog files.
   * 
   * @param changeLogLocationPattern
   */
  public void setChangeLogLocationPattern(String changeLogLocationPattern) {
    this.changeLogLocationPattern = changeLogLocationPattern;
  }

  @Override
  public void afterPropertiesSet() throws LiquibaseException {
    try {
      changeLogLocations = new ArrayList<String>();
      // retrieve all changelog resources for the pattern
      List<Resource> changeLogResources = Arrays.asList(context.getResources(changeLogLocationPattern));
      for (Resource changeLogResource : changeLogResources) {
        // get only the classpath path of the resource
        String changeLogLocation = changeLogResource.getURL().getPath();
        changeLogLocation = "classpath:" + StringUtils.substringAfterLast(changeLogLocation, "!");
        changeLogLocations.add(changeLogLocation);
    }
    // sort all found resources by string
    Collections.sort(changeLogLocations, String.CASE_INSENSITIVE_ORDER);
  } catch (IOException e) {
    throw new LiquibaseException("Could not resolve changeLogLocationPattern", e);
  }
  super.afterPropertiesSet();
}

@Override
protected SpringResourceOpener createResourceOpener() {

  final String mainChangeLog = getChangeLog();

  return new SpringResourceOpener(getChangeLog()) {

  @Override
  public InputStream getResourceAsStream(String file)
    throws IOException {
    // check if main changelog file
    if(mainChangeLog.equals(file)) {
      // load master template and convert to dom object
      Resource masterResource = getResourceLoader().getResource(file);
      Document masterDocument = DomUtils.parse(masterResource, true);
      // add all changelog locations as include elements
      for (String changeLogLocation : changeLogLocations) {
        Element inlcudeElement = masterDocument.createElement("include");
        inlcudeElement.setAttribute("file", changeLogLocation);
        masterDocument.getDocumentElement().appendChild(inlcudeElement);
      }
      if(logger.isDebugEnabled()) {
        logger.debug("Master changeset: {}", DomUtils.toString(masterDocument));
      }
      // convert dom back to string and give it back as input resource
      return new ByteArrayInputStream(DomUtils.toBytes(masterDocument));
    } else {
      return super.getResourceAsStream(file);
    }
  }

};
}

}

Этот класс теперь необходимо использовать в конфигурации spring xml.

    <bean id="liquibase" class="liquibase.integration.spring.MySpringLiquibase"
      p:changeLog="classpath:/plugin/liquibase/master.xml"
      p:dataSource-ref="dataSource"
      p:contexts="${liquibase.contexts:prod}"
      p:ignoreClasspathPrefix="true"
      p:changeLogLocationPattern="classpath*:/plugin/liquibase/changes/*.xml"/>

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

Надеюсь, это поможет и другим.

person Alex    schedule 10.04.2014