Как можно разрешить исключение ConstraintViolationException в Hibernate 3 при обновлении CollectionOfElements персистентного объекта? - PullRequest
1 голос
/ 15 июня 2010

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

Я заранее извиняюсь за длину этого вопроса, но все это связано с некоторыми довольно конкретными деталями реализации.

У меня есть следующий класс, сопоставленный с аннотациями и управляемый Hibernate 3.? (если конкретная конкретная версия окажется уместной, я выясню, что это). Java версия 1.6.

...

@Embeddable
public class JobStateChange implements Comparable<JobStateChange> {

    @Temporal(TemporalType.TIMESTAMP)
    @Column(nullable = false)
    private Date date;

    @Enumerated(EnumType.STRING)
    @Column(nullable = false, length = JobState.FIELD_LENGTH)
    private JobState state;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "acting_user_id", nullable = false)
    private User actingUser;

    public JobStateChange() {

    }

    @Override
    public int compareTo(final JobStateChange o) {
        return this.date.compareTo(o.date);
    }

    @Override
    public boolean equals(final Object obj) {
        if (this == obj) {
            return true;
        } else if (!(obj instanceof JobStateChange)) {
            return false;
        }

        JobStateChange candidate = (JobStateChange) obj;

        return this.state == candidate.state
            && this.actingUser.equals(candidate.getUser())
            && this.date.equals(candidate.getDate());
    }

    @Override
    public int hashCode() {
        return this.state.hashCode()
             + this.actingUser.hashCode()
             + this.date.hashCode();
    }

}

Он отображается как Hibernate CollectionOfElements в классе Job следующим образом:

...

@Entity
@Table(
        name = "job",
        uniqueConstraints = {
                @UniqueConstraint(
                        columnNames = {
                                "agency", //Job Name
                                "payment_type", //Job Name
                                "payment_file", //Job Name
                                "date_of_payment",
                                "payment_control_number",
                                "truck_number"
                        })
        })
public class Job implements Serializable {

    private static final long serialVersionUID = -1131729422634638834L;

    ...

    @org.hibernate.annotations.CollectionOfElements
    @JoinTable(name = "job_state", joinColumns = @JoinColumn(name = "job_id"))
    @Sort(type = SortType.NATURAL)
    private final SortedSet<JobStateChange> stateChanges = new TreeSet<JobStateChange>();

    ...

    public void advanceState(
            final User actor,
            final Date date) {
        JobState nextState;
        LOGGER.debug("Current state of {} is {}.", this, this.getCurrentState());
        if (null == this.currentState) {
            nextState = JobState.BEGINNING;
        } else {
            if (!this.isAdvanceable()) {
                throw new IllegalAdvancementException(this.currentState.illegalAdvancementStateMessage);
            }
            if (this.currentState.isDivergent()) {
                nextState = this.currentState.getNextState(this);
            } else {
                nextState = this.currentState.getNextState();
            }
        }
        JobStateChange stateChange = new JobStateChange(nextState, actor, date);
        this.setCurrentState(stateChange.getState());
        this.stateChanges.add(stateChange);
        LOGGER.debug("Advanced {} to {}", this, this.getCurrentState());
    }

    private void setCurrentState(final JobState jobState) {
        this.currentState = jobState;
    }

    boolean isAdvanceable() {
        return this.getCurrentState().isAdvanceable(this);
    }

    ...

    @Override
    public boolean equals(final Object obj) {
        if (obj == this) {
            return true;
        } else if (!(obj instanceof Job)) {
            return false;
        }

        Job otherJob = (Job) obj;

        return this.getName().equals(otherJob.getName())
            && this.getDateOfPayment().equals(otherJob.getDateOfPayment())
            && this.getPaymentControlNumber().equals(otherJob.getPaymentControlNumber())
            && this.getTruckNumber().equals(otherJob.getTruckNumber());
    }

    @Override
    public int hashCode() {
        return this.getName().hashCode()
             + this.getDateOfPayment().hashCode()
             + this.getPaymentControlNumber().hashCode()
             + this.getTruckNumber().hashCode();
    }

    ...

}

Цель JobStateChange состоит в том, чтобы записывать, когда Задание проходит через серию Изменений Состояния, которые изложены в JobState, как перечисления, которые знают о правилах повышения и уменьшения. Интерфейс, используемый для продвижения заданий через ряд состояний, заключается в вызове Job.advanceState () с датой и пользователем. Если задание можно продвигать в соответствии с правилами, закодированными в перечислении, то в SortedSet добавляется новый StateChange, и все счастливы. Если нет, генерируется исключение IllegalAdvancementException.

DDL, который генерируется следующим образом:

...

    drop table job;

    drop table job_state;

...

    create table job (
        id bigint generated by default as identity,
        current_state varchar(25),
        date_of_payment date not null,
        beginningCheckNumber varchar(8) not null,
        item_count integer,
        agency varchar(10) not null,
        payment_file varchar(25) not null,
        payment_type varchar(25) not null,
        endingCheckNumber varchar(8) not null,
        payment_control_number varchar(4) not null,
        truck_number varchar(255) not null,
        wrapping_system_type varchar(15) not null,
        printer_id bigint,
        primary key (id),
        unique (agency, payment_type, payment_file, date_of_payment, payment_control_number, truck_number)
    );

    create table job_state (
        job_id bigint not null,
        acting_user_id bigint not null,
        date timestamp not null,
        state varchar(25) not null,
        primary key (job_id, acting_user_id, date, state)
    );

...

    alter table job 
        add constraint FK19BBD12FB9D70 
        foreign key (printer_id) 
        references printer;

    alter table job_state 
        add constraint FK57C2418FED1F0D21 
        foreign key (acting_user_id) 
        references app_user;

    alter table job_state 
        add constraint FK57C2418FABE090B3 
        foreign key (job_id) 
        references job;

...

Перед выполнением тестов в базу данных добавляются следующие данные

...

insert into job (id, agency, payment_type, payment_file, payment_control_number, date_of_payment, beginningCheckNumber, endingCheckNumber, item_count, current_state, printer_id, wrapping_system_type, truck_number)
values (-3, 'RRB', 'Monthly', 'Monthly','4501','1998-12-01 08:31:16' , '00000001','00040000', 40000, 'UNASSIGNED', null, 'KERN', '02');

insert into job_state (job_id, acting_user_id, date, state)
values (-3, -1, '1998-11-30 08:31:17', 'UNASSIGNED');

...

После того, как схема базы данных будет автоматически сгенерирована и перестроена с помощью инструмента Hibernate.

Следующий тест работает нормально до вызова Session.flush ()

...

@ContextConfiguration(locations = { "/applicationContext-data.xml", "/applicationContext-service.xml" })
public class JobDaoIntegrationTest
extends AbstractTransactionalJUnit4SpringContextTests {

    @Autowired
    private JobDao jobDao;

    @Autowired
    private SessionFactory sessionFactory;

    @Autowired
    private UserService userService;

    @Autowired
    private PrinterService printerService;

...

    @Test
    public void saveJob_JobAdvancedToAssigned_AllExpectedStateChanges() {
        //Get an unassigned Job
        Job job = this.jobDao.getJob(-3L);

        assertEquals(JobState.UNASSIGNED, job.getCurrentState());
        Date advancedToUnassigned = new GregorianCalendar(1998, 10, 30, 8, 31, 17).getTime();
        assertEquals(advancedToUnassigned, job.getStateChange(JobState.UNASSIGNED).getDate());

        //Satisfy advancement constraints and advance
        job.setPrinter(this.printerService.getPrinter(-1L));

        Date advancedToAssigned = new Date();
        job.advanceState(
                this.userService.getUserByUsername("admin"),
                advancedToAssigned);

        assertEquals(JobState.ASSIGNED, job.getCurrentState());

        assertEquals(advancedToUnassigned, job.getStateChange(JobState.UNASSIGNED).getDate());
        assertEquals(advancedToAssigned, job.getStateChange(JobState.ASSIGNED).getDate());

        //Persist to DB
        this.sessionFactory.getCurrentSession().flush();

...
    }

...

}

Ошибка: SQLCODE = -803, SQLSTATE = 23505:

could not insert collection rows: [jaci.model.job.Job.stateChanges#-3]

org.hibernate.exception.ConstraintViolationException: could not insert collection rows: [jaci.model.job.Job.stateChanges#-3]
at org.hibernate.exception.SQLStateConverter.convert(SQLStateConverter.java:94)
at org.hibernate.exception.JDBCExceptionHelper.convert(JDBCExceptionHelper.java:66)
at org.hibernate.persister.collection.AbstractCollectionPersister.insertRows(AbstractCollectionPersister.java:1416)
at org.hibernate.action.CollectionUpdateAction.execute(CollectionUpdateAction.java:86)
at org.hibernate.engine.ActionQueue.execute(ActionQueue.java:279)
at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:263)
at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:170)
at org.hibernate.event.def.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:321)
at org.hibernate.event.def.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:50)
at org.hibernate.impl.SessionImpl.flush(SessionImpl.java:1027)
at jaci.dao.JobDaoIntegrationTest.saveJob_JobAdvancedToAssigned_AllExpectedStateChanges(JobDaoIntegrationTest.java:98)
at org.springframework.test.context.junit4.SpringTestMethod.invoke(SpringTestMethod.java:160)
at org.springframework.test.context.junit4.SpringMethodRoadie.runTestMethod(SpringMethodRoadie.java:233)
at org.springframework.test.context.junit4.SpringMethodRoadie$RunBeforesThenTestThenAfters.run(SpringMethodRoadie.java:333)
at org.springframework.test.context.junit4.SpringMethodRoadie.runWithRepetitions(SpringMethodRoadie.java:217)
at org.springframework.test.context.junit4.SpringMethodRoadie.runTest(SpringMethodRoadie.java:197)
at org.springframework.test.context.junit4.SpringMethodRoadie.run(SpringMethodRoadie.java:143)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.invokeTestMethod(SpringJUnit4ClassRunner.java:160)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:97)
Caused by: com.ibm.db2.jcc.b.lm: DB2 SQL Error: SQLCODE=-803, SQLSTATE=23505, SQLERRMC=1;ACI_APP.JOB_STATE, DRIVER=3.50.152
at com.ibm.db2.jcc.b.wc.a(wc.java:575)
at com.ibm.db2.jcc.b.wc.a(wc.java:57)
at com.ibm.db2.jcc.b.wc.a(wc.java:126)
at com.ibm.db2.jcc.b.tk.b(tk.java:1593)
at com.ibm.db2.jcc.b.tk.c(tk.java:1576)
at com.ibm.db2.jcc.t4.db.k(db.java:353)
at com.ibm.db2.jcc.t4.db.a(db.java:59)
at com.ibm.db2.jcc.t4.t.a(t.java:50)
at com.ibm.db2.jcc.t4.tb.b(tb.java:200)
at com.ibm.db2.jcc.b.uk.Gb(uk.java:2355)
at com.ibm.db2.jcc.b.uk.e(uk.java:3129)
at com.ibm.db2.jcc.b.uk.zb(uk.java:568)
at com.ibm.db2.jcc.b.uk.executeUpdate(uk.java:551)
at org.hibernate.jdbc.NonBatchingBatcher.addToBatch(NonBatchingBatcher.java:46)
at org.hibernate.persister.collection.AbstractCollectionPersister.insertRows(AbstractCollectionPersister.java:1389)

В этом и заключается моя проблема & hellip; Практически идентичный набор классов (на самом деле, настолько идентичный, что я немного ломал голову, чтобы сделать его единым классом, обслуживающим обе бизнес-сущности) работает абсолютно нормально. Это идентично, кроме имени. Вместо работы это веб. Вместо JobStateChange это WebStateChange. Вместо JobState это WebState. SortedSet StateChanges для Job и Web отображаются как Hibernate CollectionOfElements. Оба @Embeddable. Оба SortType.Natural. Оба поддерживаются Перечислением с некоторыми правилами продвижения. И все же, когда для Web выполняется почти идентичный тест, проблема не обнаруживается, и данные сбрасываются нормально. Для краткости я не буду включать здесь все веб-классы, но я включу тест и, если кто-то захочет увидеть фактические источники, я включу их (просто оставьте комментарий).

Семя данных:

insert into web (id, stock_type, pallet, pallet_id, date_received, first_icn, last_icn, shipment_id, current_state)
values (-1, 'PF', '0011', 'A', '2008-12-31 08:30:02', '000000001', '000080000', -1, 'UNSTAGED');

insert into web_state (web_id, date, state, acting_user_id)
values (-1, '2008-12-31 08:30:03', 'UNSTAGED', -1);

Тест:

...

@ContextConfiguration(locations = { "/applicationContext-data.xml", "/applicationContext-service.xml" })
public class WebDaoIntegrationTest
extends AbstractTransactionalJUnit4SpringContextTests {

    @Autowired
    private WebDao webDao;

    @Autowired
    private UserService userService;

    @Autowired
    private SessionFactory sessionFactory;

...

    @Test
    public void saveWeb_WebAdvancedToNewState_AllExpectedStateChanges() {
        Web web = this.webDao.getWeb(-1L);
        Date advancedToUnstaged = new GregorianCalendar(2008, 11, 31, 8, 30, 3).getTime();

        assertEquals(WebState.UNSTAGED, web.getCurrentState());
        assertEquals(advancedToUnstaged, web.getState(WebState.UNSTAGED).getDate());

        Date advancedToStaged = new Date();
        web.advanceState(
                this.userService.getUserByUsername("admin"),
                advancedToStaged);

        this.sessionFactory.getCurrentSession().flush();

        web = this.webDao.getWeb(web.getId());

        assertEquals(
                "Web should have moved to STAGED State.",
                WebState.STAGED,
                web.getCurrentState());
        assertEquals(advancedToUnstaged, web.getState(WebState.UNSTAGED).getDate());
        assertEquals(advancedToStaged, web.getState(WebState.STAGED).getDate());

        assertNotNull(web.getState(WebState.UNSTAGED));
        assertNotNull(web.getState(WebState.STAGED));
    }

...

}

Как вы можете видеть, я утверждаю, что сеть была восстановлена ​​так, как я ожидаю, я продвигаю ее, сбрасываю в БД, а затем повторно получаю и проверяю, что состояния соответствуют ожидаемым. Все работает отлично. Не так с Иовом.

Возможно, важная деталь: код восстановления работает нормально, если я перестаю отображать JobStateChange.data как TIMESTAMP и вместо этого как DATE, и гарантируют, что все StateChanges всегда происходят в разные даты. Проблема заключается в том, что данный конкретный бизнес-объект может проходить через много изменений состояния в течение одного дня, и поэтому его нужно отсортировать по отметке времени, а не по дате. Если я этого не сделаю, я не могу правильно отсортировать StateChanges. При этом WebStateChange.date также отображается как TIMESTAMP, и поэтому я снова остаюсь совершенно сбитым с толку относительно того, откуда возникает эта ошибка.

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

Большое спасибо за вашу помощь!

ОБНОВЛЕНИЕ : Поскольку это оказывается важным для решения моей проблемы, я должен также включить соответствующие биты класса WebStateChange.

1 Ответ

3 голосов
/ 16 июня 2010

Метод equals в JobStateChange использует прямой доступ к полю. Изменение его для использования геттеров для различных свойств решит проблему. Вы также можете рассмотреть возможность использования метода HibernateProxyHelper.getClassWithoutInitializingProxy при выполнении сравнения instanceof.

Например, метод JobStateChange.equals может выглядеть следующим образом:

@Override
public boolean equals(final Object obj) {
    if (this == obj) {
        return true;
    } else if (!(HibernateProxyHelper.getClassWithoutInitializingProxy(obj) 
                 instanceof JobStateChange)) {
        return false;
    }

    JobStateChange candidate = (JobStateChange) obj;

    return this.getState() == candidate.getState()
        && this.getActingUser().equals(candidate.getUser())
        && this.getDate().equals(candidate.getDate());
}

Аналогично, метод JobStateChange.hashCode() должен также использовать методы получения (я бы также рекомендовал написать метод hashCode, чтобы соответствовать алгоритму, предложенному Джошуа Блохом в Effective Java, глава 3 (начиная со страницы 38), но на самом деле это не имеет отношения к вопросу):

@Override
public int hashCode() {
    return this.getState().hashCode()
         + this.getActingUser().hashCode()
         + this.getDate().hashCode();
}

Частью "магии" Hibernate являются динамические прокси. Во многих случаях Hibernate создает (во время выполнения) подкласс ваших классов сущностей и переопределяет методы получения и установки постоянных свойств. Таким образом, вы не можете ссылаться на свойства непосредственно в equals и hashCode, но вместо этого должны обращаться к ним, используя методы получения и установки свойств, даже внутри класса сущности .

Вы получили ошибку «уникальное ограничение», поскольку Hibernate полагается на метод equals при сохранении изменений в коллекции.

При старом методе equals проксированные JobStateChange объекты никогда не будут равны друг другу или непроксифицированным JobStateChange объектам. Таким образом, Hibernate подумал, что существующие элементы в коллекции stateChanges были новыми элементами, и попытался вставить их в базу данных. Поскольку строки в job_state должны быть уникальными (определяется первичным ключом во всех столбцах), было создано ограничение ограничения.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...