Как убедиться, что изменения в базе данных, которые происходят в транзакции, также видны всем в этой транзакции - PullRequest
0 голосов
/ 25 октября 2010

У меня есть транзакция, которая сохраняет или обновляет набор объектов в базе данных.Это код:

@Transactional
public void updatePDBEntry(Set<PDBEntry> pdbEntrySet) {
    for (PDBEntry pdbEntry : pdbEntrySet) {
        PDBEntry existingEntry = findByAccessionCode(pdbEntry.getAccessionCode());
        if (existingEntry != null) {
            log.debug("Remove previous version of PDBEntry {}", existingEntry);
            makeTransient(existingEntry);
        }
        makePersistent(pdbEntry);
    }
}

И в genericDAO:

public void makePersistent(I entity) {
    getCurrentSession().saveOrUpdate(entity);
}

public void makeTransient(I entity) {
    getCurrentSession().delete(entity);
}

Почему-то это не работает, он говорит, что не может вставить объект из-за дублированного ключа,хотя я могу видеть в журналах, что он получает makeTransient ().Я предполагаю, что это связано с тем, что все это происходит внутри транзакции, поэтому изменения, сделанные makeTransient (), могут быть не видны методом makePersistent ().Я мог бы решить эту проблему, скопировав все данные из pdbEntry в существующийEntry, а затем выполнив saveOrUpdate (существующийEntry), но это своего рода грязный хак.Есть ли другой способ убедиться, что makeTransient видим для makePersistent, при этом сохраняя все это внутри транзакции?

EDIT: Это моя модель домена PDBEntry:

@Entity
@Data
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@EqualsAndHashCode(callSuper = false, of = { "accessionCode", "date" })
@SuppressWarnings("PMD.UnusedPrivateField")
public class PDBEntry extends DomainObject implements Serializable {
    @NaturalId
    @NotEmpty
    @Length(max = 4)
    private String          accessionCode;

    @NaturalId
    @NotNull
    @Temporal(TemporalType.DATE)
    private Date            date;

    private String          header;

    private Boolean         isValidDssp;

    @Temporal(TemporalType.TIMESTAMP)
    private Date            lastUpdated     = new Date(System.currentTimeMillis());

    @OneToOne(mappedBy = "pdbEntry", cascade = CascadeType.ALL)
    private ExpMethod       expMethod;

    @OneToMany(mappedBy = "pdbEntry", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private Set<Refinement> refinementSet   = new HashSet<Refinement>();

    @OneToMany(mappedBy = "pdbEntry", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private Set<HetGroup>   hetGroupSet     = new HashSet<HetGroup>();

    @OneToMany(mappedBy = "pdbEntry", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private Set<Chain>      chainSet        = new HashSet<Chain>();

    @OneToMany(mappedBy = "pdbEntry", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private Set<Chain>      residueSet      = new HashSet<Chain>();

    public Date getLastUpdated() {
        return new Date(lastUpdated.getTime());
    }

    public void setLastUpdated() throws InvocationTargetException {
        throw new InvocationTargetException(new Throwable());
    }

    public void touch() {
        lastUpdated = new Date(System.currentTimeMillis());
    }

    @Override
    public String toString() {
        return accessionCode;
    }

    public PDBEntry(String accessionCode, Date date) throws NullPointerException {
        if (accessionCode != null && date != null) {
            this.accessionCode = accessionCode;
            this.date = date;
        } else {
            throw new NullPointerException();
        }
    }
}

@MappedSuperclass
public abstract class DomainObject implements Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long    id;

    public Long getId() {
        return id;
    }

    @Override
    public abstract boolean equals(Object obj);

    @Override
    public abstract int hashCode();

    @Override
    public abstract String toString();
}

EDIT: Newпроблема, я создал метод, который сначала удаляет все существующие объекты из базы данных следующим образом:

@Override
@Transactional
public void updatePDBEntries(Set<PDBEntry> pdbEntrySet) {
    findAndRemoveExistingPDBEntries(pdbEntrySet);
    savePDBEntries(pdbEntrySet);
}

@Override
@Transactional
public void findAndRemoveExistingPDBEntries(Set<PDBEntry> pdbEntrySet) {
    for (PDBEntry pdbEntry : pdbEntrySet) {
        PDBEntry existingPDBEntry = findByAccessionCode(pdbEntry.getAccessionCode());
        if (existingPDBEntry != null) {
            log.info("Delete: {}", pdbEntry);
            makeTransient(existingPDBEntry);
        }
    }
}

@Override
@Transactional
public void savePDBEntries(Set<PDBEntry> pdbEntrySet) {
    for (PDBEntry pdbEntry : pdbEntrySet) {
        log.info("Save: {}", pdbEntry);
        makePersistent(pdbEntry);
    }
}

Кажется, что удаляет первые 73 записи, которые он встречает, но затем выдает ошибку:

WARN  2010-10-25 14:28:49,406 main JDBCExceptionReporter:100 - SQL Error: 0, SQLState: 23503
ERROR 2010-10-25 14:28:49,406 main JDBCExceptionReporter:101 - Batch entry 0 /* delete nl.ru.cmbi.pdbeter.core.model.domain.PDBEntry */ delete from PDBEntry where id='74' was aborted.  Call getNextException to see the cause.
WARN  2010-10-25 14:28:49,406 main JDBCExceptionReporter:100 - SQL Error: 0, SQLState: 23503
ERROR 2010-10-25 14:28:49,406 main JDBCExceptionReporter:101 - ERROR: update or delete on table "pdbentry" violates foreign key constraint "fke03a2dc84d44e296" on table "hetgroup"
  Detail: Key (id)=(74) is still referenced from table "hetgroup".
ERROR 2010-10-25 14:28:49,408 main AbstractFlushingEventListener:324 - Could not synchronize database state with session
org.hibernate.exception.ConstraintViolationException: Could not execute JDBC batch update

Есть идеи, как эта ошибка может возникнуть?

РЕДАКТИРОВАТЬ: Я обнаружил проблему: последняя ошибка была вызвана моделью гетерогенной группы, которая выглядит следующим образом:

@Entity
@Data
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@EqualsAndHashCode(callSuper = false, of = { "pdbEntry", "hetId" })
@SuppressWarnings("PMD.UnusedPrivateField")
// extends DomainObject which contains Id, NaturalId is not enough in this case, since duplicate chains still exist
// in fact this is an error of the PDBFinder and will be fixed in the future
public class HetGroup extends DomainObject implements Serializable {
    //@NaturalId 
    @NotNull
    @ManyToOne
    private PDBEntry    pdbEntry;

    //@NaturalId 
    @NotEmpty
    private String hetId;

    private  Integer nAtom;

    @Length(max = 8192)
    private String name;

    public HetGroup(PDBEntry pdbEntry, String hetId) {
        this.pdbEntry = pdbEntry;
        pdbEntry.getHetGroupSet().add(this);

        this.hetId = hetId;
    }
}

Особенно обратите внимание на комментарии @ NaturalId, я прокомментировал их, потому что в моих данных все еще была некоторая ошибка, которая вызвала дублирование гетерогенных групп, поэтому я решил, что на данный момент я просто удалил для них ограничение UNIQUE, но я забыл, что используюЛомбок для создания методов равенства и хэш-кода для меня, как видно из строки:

@EqualsAndHashCode(callSuper = false, of = { "pdbEntry", "hetId" })

Это вызвало ошибку с дублирующимися HetId.Я исправил данные и восстановил @ NaturalId, и теперь все работает нормально.

Спасибо всем!

Ответы [ 2 ]

2 голосов
/ 25 октября 2010

Почему-то это не работает, он говорит, что не может вставить объект из-за дублированного ключа, хотя я вижу в журналах, что он получает makeTransient ().

Чтобы понять, что здесь происходит, вы должны сначала понять, что Hibernate не сразу записывает изменения в базу данных, изменения помещаются в очередь в сеансе и записываются в flush время .Таким образом, даже если вы видите, что вызывается makeTransient(), это не означает, что соответствующая запись была фактически удалена из базы данных во время вызова метода.Оператор удаления SQL и другие ожидающие изменения будут выполнены, когда произойдет flush (либо путем явного вызова flush(), во время commit(), либо при выполнении запроса HQL ),Это хорошо объясняется в документации:

10.10.Сброс сеанса

Иногда сеанс выполняет SQL-операторы, необходимые для синхронизации состояния соединения JDBC с состоянием объектов, находящихся в памяти.Этот процесс, называемый flush , по умолчанию происходит в следующих точках:

  • перед выполнением некоторых запросов
  • из org.hibernate.Transaction.commit()
  • из Session.flush()

Операторы SQL выдаются в следующем порядке:

  1. все вставки сущностей в том же порядке соответствующих объектовбыли сохранены с использованием session.save()
  2. всех обновлений сущностей
  3. всех удалений коллекций
  4. всех удалений, обновлений и вставок всех элементов коллекции
  5. всех вставок коллекции
  6. все удаления сущностей в том же порядке, соответствующие объекты были удалены с использованием Session.delete()

Исключением является то, что объекты, использующие генерацию собственных идентификаторов, вставляются, когда онисохранены

...

Итак, давайте еще раз посмотрим на ваш код:

01: @Transactional
02: public void updatePDBEntry(Set<PDBEntry> pdbEntrySet) {
03:     for (PDBEntry pdbEntry : pdbEntrySet) {
04:         PDBEntry existingEntry = findByAccessionCode(pdbEntry.getAccessionCode());
05:         if (existingEntry != null) {
06:             log.debug("Remove previous version of PDBEntry {}", existingEntry);
07:             makeTransient(existingEntry);
08:         }
09:         makePersistent(pdbEntry);
10:     }
11: }
  • на 04, вы выполните запрос (это будет сбрасыватьв ожидании изменений, если таковые имеются)
  • в 07, вы выполняете session.delete() (в памяти)
  • в 09, вы выполняете session.save() (в памяти)
  • назад04, изменения сбрасываются, и Hibernate выдает операторы SQL в указанном выше порядке
    • , и вставка не удалась из-за нарушения ключа, поскольку запись еще не была удалена

Другими словами, ваша проблема не имеет ничего общего с транзакциями, она просто связана с тем, как работает Hibernate и как вы его используете.

Есть ли другой способ убедиться, чтоmakeTransient виден makePersistent, все еще сохраняя все в транзакции?

Ну, не меняя логику, вы могли бы помочь Hibernate и flush() явно после delete().

Но ИМО, весь подход является неоптимальным, и вы должны обновить еxisting запись, если есть, вместо delete затем вставьте (если нет веских причин для этого).

Ссылки

2 голосов
/ 25 октября 2010

Одной транзакции не должно быть проблемой.

Ваше предложение - скопировать данные из pdbEntry в существующий Entry - гораздо лучшее решение.Это намного менее требовательно к базам данных и немного легче читать и понимать, что происходит.

Кроме того, если бы вы сделали это таким образом, вам не пришлось бы делать saveOrUpdate для измененного объекта.Hibernate реализует то, что известно как «прозрачное постоянство» ... что означает, что hibernate отвечает за определение того, какие операции с данными необходимо использовать для синхронизации объекта с базой данных.Операция «Обновить» в спящем режиме не предназначена для объектов, которые уже являются постоянными.см. здесь

Код в этом случае будет выглядеть следующим образом (примечание не требуется обновлять):

    public void updatePDBEntry(Set<PDBEntry> pdbEntrySet) {
        for (PDBEntry pdbEntry : pdbEntrySet) {
            PDBEntry existingEntry = findByAccessionCode(pdbEntry.getAccessionCode());
            if (existingEntry != null) {
                // copy relevant fields from pdbEntry to existing Entry - preferably with a method on PDBEntry
            } else {
                makePersistent(pdbEntry); // although better to just call _save_ (not saveOrUpdate) cause you know it's a new object
            }
        }            
    }
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...