GORM предотвращает создание ограничения внешнего ключа для домена

#grails #grails-orm #foreign-key-relationship

#grails #grails-orm #foreign-key-relationship

Вопрос:

Я разрабатываю веб-приложение на Grails. Я столкнулся с ситуацией, когда я хотел бы попытаться запретить GORM создавать ограничение внешнего ключа для поля в таблице.

У меня есть класс домена, который является частью иерархии классов. Класс domain, по сути, действует как ссылка на целевой домен. Целевой домен может быть разных типов, и каждый из подклассов этого ссылочного домена предназначен для предоставления ссылки для каждого конкретного типа связанного элемента. Эти связанные элементы имеют определенное общее поведение, т. е. реализуют один и тот же интерфейс, но в остальном отличаются настолько, что хранятся в разных таблицах.

В этой таблице домена ссылок есть один столбец, который представляет идентификатор элемента, с которым выполняется ссылка. Все связанные элементы имеют одинаковый идентификатор на основе целого числа. Проблема в том, что GORM пытается создать несколько ограничений внешнего ключа для одного и того же столбца таблицы, по одному для каждого из подклассов домена ссылок, который представляет другой тип связанного элемента. Я знаю, что у меня могли бы быть отдельные столбцы для id каждого раза, когда другие столбцы id были бы равны null, но это кажется немного запутанным. Если бы был способ просто сообщить GORM, что я не хочу, чтобы он создавал ограничение внешнего ключа для этого столбца (потому что разные внешние ключи используют один и тот же столбец), это решило бы проблему.

Я знаю, что возникает вопрос о ссылочной целостности и о том, можно ли поместить значение ключа ссылки в столбец, которого не существует во внешней таблице, но приложение должно предотвратить возникновение этой ситуации.

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

Комментарии:

1. не могли бы вы, пожалуйста, опубликовать некоторые классы и ожидаемые таблицы? Вы подробно описали, что вы хотите, но с классами это лучше. Возможно, я смогу помочь больше, чем мой первый ответ, если вы сделаете это.

Ответ №1:

После относительно короткого поиска в Google я нашел запись в блоге Берта Беквита: http://burtbeckwith.com/blog/?p=465 это объясняет основы настройки GORM. С помощью следующего класса конфигурации мне удалось предотвратить создание ключа, который я не хотел создавать. В примере Burt требуется RootClass, но это не соответствует моим потребностям, поэтому проверка опущена.

 package com.myapp;

import com.myapp.objects.SomeClass;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.codehaus.groovy.grails.orm.hibernate.cfg.GrailsAnnotationConfiguration;
import org.hibernate.MappingException;
import org.hibernate.mapping.ForeignKey;
import org.hibernate.mapping.PersistentClass;
import org.hibernate.mapping.RootClass;

import java.util.Collection;
import java.util.Iterator;

public class DomainConfiguration extends GrailsAnnotationConfiguration {
    private static final long serialVersionUID = 1;

    private boolean _alreadyProcessed;

    @SuppressWarnings({"unchecked", "rawtypes"})
    @Override
    protected void secondPassCompile() throws MappingException {
        super.secondPassCompile();

        if(_alreadyProcessed) {
            return;
        }

        Log log = LogFactory.getLog(DomainConfiguration.class.getName());

        for(PersistentClass pc : (Collection<PersistentClass>) classes.values()) {
            boolean preventFkCreation = false;
            String fkReferencedEntityNameToPrevent = null;

            if("com.myapp.objects.SomeClassWithUnwantedFkThatHasSomeClassAsAMember".equals(pc.getClassName())) {
                preventFkCreation = true;
                fkReferencedEntityNameToPrevent = SomeClass.class.getName();
            }

            if(preventFkCreation) {
                for(Iterator iter = pc.getTable().getForeignKeyIterator(); iter.hasNext(); ) {
                    ForeignKey fk = (ForeignKey) iter.next();

                    if(fk.getReferencedEntityName().equals(fkReferencedEntityNameToPrevent)) {
                        iter.remove();
                        log.info("Prevented creation of foreign key referencing "   fkReferencedEntityNameToPrevent   " in "   pc.getClassName()   ".");
                    }
                }
            }
        }

        _alreadyProcessed = true;
    }
}
  

Класс конфигурации вводится в Grails путем помещения его в datasource.groovy:

 dataSource {
    ...
    ...
    configClass = 'com.myapp.DomainConfiguration
}
  

Комментарии:

1. Есть ли у вас аналогичное решение для Grails 3 с Hibernate 5?

2. @Viriato К сожалению, нет.

3. Я добавил еще один ответ для Grails 3 / Hibernate 5.

Ответ №2:

Для тех, кто борется с этой проблемой, используя Grails 3 с gorm-hibernate5, я нашел решение, основанное на комментарии Грэма к grails-data-mapping #880.

Я реализовал пользовательский SchemaManagementTool и добавил его в конфигурацию приложения:

 hibernate.schema_management_tool = CustomSchemaManagementTool
  

Инструмент Hibernate SchemaManagementTool в конечном итоге делегирует необработанные SQL-команды в GenerationTarget (обычно GenerationTargetToDatabase), поэтому наша цель — предоставить нашу собственную GenerationTarget.

Это было бы проще всего, если бы мы могли переопределить HibernateSchemaManagementTool.buildGenerationTargets, но это, к сожалению, не доступно. Вместо этого нам нужно разработать наши собственные SchemaCreator и SchemaDropper и вернуть их в CustomSchemaManagementTool:

 class CustomSchemaManagementTool extends HibernateSchemaManagementTool {
    @Override
    SchemaCreator getSchemaCreator(Map options) {
        return new CustomSchemaCreator(this, getSchemaFilterProvider(options).getCreateFilter())
    }

    @Override
    SchemaDropper getSchemaDropper(Map options) {
        return new CustomSchemaDropper(this, getSchemaFilterProvider(options).getDropFilter())
    }

    // We unfortunately copy this private method from HibernateSchemaManagementTool
    private SchemaFilterProvider getSchemaFilterProvider(Map options) {
        final Object configuredOption = (options == null) ? null : options.get(AvailableSettings.HBM2DDL_FILTER_PROVIDER)
        return serviceRegistry.getService(StrategySelector.class).resolveDefaultableStrategy(
            SchemaFilterProvider.class,
            configuredOption,
            DefaultSchemaFilterProvider.INSTANCE
        )
    }
}
  

Для реализации SchemaCreator и SchemaDropper мы можем переопределить doCreation и doDrop соответственно. По сути, они скопированы из реализаций Hibernate, но с пользовательским параметром GENERATIONTARGET вместо GenerationTargetToDatabase:

 class CustomSchemaCreator extends SchemaCreatorImpl {
    private final HibernateSchemaManagementTool tool

    CustomSchemaCreator(HibernateSchemaManagementTool tool, SchemaFilter schemaFilter) {
        super(tool, schemaFilter)
        this.tool = tool
    }

    @Override
    void doCreation(Metadata metadata, ExecutionOptions options, SourceDescriptor sourceDescriptor, TargetDescriptor targetDescriptor) {
        final JdbcContext jdbcContext = tool.resolveJdbcContext( options.getConfigurationValues() )
        final GenerationTarget[] targets = new GenerationTarget[ targetDescriptor.getTargetTypes().size() ]
        targets[0] = new CustomGenerationTarget(tool.getDdlTransactionIsolator(jdbcContext), true)
        super.doCreation(metadata, jdbcContext.getDialect(), options, sourceDescriptor, targets)
    }
}

class CustomSchemaDropper extends SchemaDropperImpl {
    private final HibernateSchemaManagementTool tool

    CustomSchemaDropper(HibernateSchemaManagementTool tool, SchemaFilter schemaFilter) {
        super(tool, schemaFilter)
        this.tool = tool
    }

    @Override
    void doDrop(Metadata metadata, ExecutionOptions options, SourceDescriptor sourceDescriptor, TargetDescriptor targetDescriptor) {
        final JdbcContext jdbcContext = tool.resolveJdbcContext( options.getConfigurationValues() )
        final GenerationTarget[] targets = new GenerationTarget[ targetDescriptor.getTargetTypes().size() ]
        targets[0] = new CustomGenerationTarget(tool.getDdlTransactionIsolator(jdbcContext), true)
        super.doDrop(metadata, options, jdbcContext.getDialect(), sourceDescriptor, targets)
    }
}
  

В этом случае я использую один и тот же CustomGenerationTarget как для create, так и для drop, но вы могли бы легко разделить это на разные классы. Теперь мы, наконец, получаем выгоду, расширяя базу данных GenerationTargetToDatabase и переопределяя метод accept. Только вызывая инструкции super.accept для SQL для сохранения, вы можете отфильтровать нежелательные инструкции DDL.

 class CustomGenerationTarget extends GenerationTargetToDatabase {
    CustomGenerationTarget(DdlTransactionIsolator ddlTransactionIsolator, boolean releaseAfterUse) {
        super(ddlTransactionIsolator, releaseAfterUse)
    }

    @Override
    void accept(String command) {
        if (shouldAccept(command))
            super.accept(command)
    }

    boolean shouldAccept(String command) {
        // Custom filtering logic here, e.g.:
        if (command =~ /references legacy.xyz/)
            return false
        return true
    }
}
  

Это не самое элегантное решение, но вы можете выполнить свою работу.

Я также попытался предоставить свой собственный SchemaFilterProvider (и пользовательский SchemaFilter). К сожалению, это позволяет фильтровать только таблицы / пространства имен, а не внешние ключи.

Комментарии:

1. Я попробую это сделать, но где в вашем решении указано удалить внешние ключи? Мне это непонятно, если посмотреть на предоставленные вами 4 класса. В приведенном выше решении grails 2 у меня также есть это, так что оно удаляет checkconstraints, индексирует уникальные ключи и столбцы из объекта PersistentClass из доменов в определенном пакете. Это делается для того, чтобы убедиться, что наши сопоставленные таблицы поставщиков никоим образом не изменяются Grails (даже в среде разработки).

2. Внешние ключи фильтруются (не передаются super) в методе CustomGenerationTarget.accept. В этом случае я ищу инструкции SQL «references legacy.xyz» (оператор внешнего ключа PostgreSQL).

3. итак, мы смогли протестировать это и смогли отфильтровать генерацию fk при запуске в режиме «create-drop», но когда мы запускаемся в режиме dbCreate = «update», метод accept никогда не попадает. Есть мысли?

4. В @Viriato SchemaManagementTool также есть метод getSchemaMigrator, который я не переопределял в этом примере. Может быть, это вызывается для dbCreate = «update»?

5. вы успешно заставили это работать в вашем проекте? Я добавил getSchemaMigrator в инструмент пользовательского управления и сказал ему использовать CustomSchemaMigrator, который принимает targets[0] = new CustomGenerationTarget в переопределенном способе миграции, но я сталкиваюсь с внутренними исключениями, какие-либо мысли?

Ответ №3:

вы смотрели этот раздел документации

http://grails.org/doc/latest/guide/5. Object Relational Mapping (GORM).html#5.5.2 Пользовательское отображение ORM

вы можете переопределить семантику сохраняемости grails по умолчанию с помощью пользовательского отображения DSL.

Комментарии:

1. Я прочитал этот раздел и уже довольно широко использую пользовательское отображение DSL в своем приложении. Я некоторое время просматривал документацию, но не нашел упоминания о способах изменения поведения при создании внешнего ключа. Я начинаю подозревать, что то, что я хочу сделать, в настоящее время невозможно.