Двойная проблема с ключами в Spring и Hibernate - требуется помощь - PullRequest
0 голосов
/ 13 февраля 2010

- Резюме (сокращено) -

У меня есть контроллер, который загружает объект профиля из соответствующего DAO. Он обновляет некоторые свойства, многие из них устанавливаются, а затем вызывает saveOrUpdate (через save в DAO) для повторного присоединения и обновления объекта профиля. Через, казалось бы, случайные интервалы мы получаем org.hibernate.exception.ConstraintViolationException с основной причиной: вызвано: java.sql.BatchUpdateException: повторяющаяся запись '3-56' для ключа 1. Трассировка стека указывает на метод saveOrUpdate, который называется из контроллера обновления профиля. Я не могу выполнить репликацию в моей тестовой среде, мы видим это только в производственной среде, поэтому мне интересно, если я что-то упустил из-за безопасности потоков (именно поэтому я публикую так много информации о коде / конфигурации). Есть идеи?

- Код -

Я пытался предоставить как можно больше соответствующей конфигурации / кода - дайте мне знать, если потребуется больше:

Вот выдержка из нарушающего контроллера:

public class EditProfileController extends SimpleFormController {

protected ModelAndView onSubmit(HttpServletRequest request, HttpServletResponse response, Object command, BindException errors) throws Exception
{
    if(!checkLoggedIn(request))
    {
        return new ModelAndView("redirect:" + invalidRedirect);
    }

    HttpSession session = request.getSession();
    Resource resource = (Resource)session.getAttribute("resource"); //The resource object is stored in session upon login and upon account creation.
    Profile profile = profiles.getProfileByResource(resource);

    if(profile == null)
    {
        profile = new Profile();
        profile.setResource(resource);
    }

    //I use custom editors to populate the sets in the command object with objects based on the selection

    if(profile.getPrimaryRoleSkills() != null && editProfileCommand.getPrimaryRoleSkills() != null)
    {
        profile.getPrimaryRoleSkills().addAll(editProfileCommand.getPrimaryRoleSkills());
        profile.getPrimaryRoleSkills().retainAll(editProfileCommand.getPrimaryRoleSkills());
    }
    else
        profile.setPrimaryRoleSkills(editProfileCommand.getPrimaryRoleSkills());

    profiles.save(profile);  //This is the line that appears in the stack trace
    return new ModelAndView(getSuccessView());
}
//Other methods omitted
}

Сокращенный профиль класса:

public class Profile  implements java.io.Serializable {

private long id;
private Resource resource;
private Set<PrimaryRoleSkill> primaryRoleSkills = new HashSet<PrimaryRoleSkill>(0);

public Profile() {
}
//Other properties trivial or similar to above.  Getters and setters omitted
//toString, equals, and hashCode are all generated by hbm2java
}

Базовый класс NameValuePairs (PrimaryRoleSkill расширяет это, ничего не добавляя):

public class NameValuePairs  implements java.io.Serializable {
private long id;
private String name;
private boolean active = true;

public NameValuePairs() {
}
//equals and hashCode generated by hbm2java, getters & setters omitted
} 

Вот мой базовый класс DAO:

public class DAO {

protected DAO() {
}   

public static Session getSession() {
      Session session = (Session) DAO.session.get();
      if (session == null) {
         session = sessionFactory.openSession();
         DAO.session.set(session);
      }
      return session;
}

protected void begin() {
  getSession().beginTransaction();
}

protected void commit() {
  getSession().getTransaction().commit();
}

protected void rollback() {
  try {
  getSession().getTransaction().rollback();
  } catch( HibernateException e ) {
     log.log(Level.WARNING,"Cannot rollback",e);
  }

  try {
     getSession().close();
  } catch( HibernateException e ) {
     log.log(Level.WARNING,"Cannot close",e);
  }
  DAO.session.set(null);
 }

public boolean save(Object object)
{
   try {
        begin();
        getSession().saveOrUpdate(object);
        commit();
        return true;
    }
    catch (HibernateException e) {
        log.log(Level.WARNING,"Cannot save",e);
        rollback();
        return false;
    }
}

private static final ThreadLocal<Session> session = new ThreadLocal<Session>();

private static final SessionFactory sessionFactory = new Configuration()
     .configure().buildSessionFactory();
private static final Logger log = Logger.getAnonymousLogger();

//Non-related methods omitted.
}

Ниже приведена важная часть профилей DAO:

public class Profiles extends DAO {
public Profile getProfileByResource(Resource resource)
{
    try
    {
        begin();
        Query q = getSession().createQuery("from Profile where resource = :resource");
        q.setLong("resource", resource.getId());
        commit();
        if(q.uniqueResult() == null)
            return null;

        return (Profile) q.uniqueResult();
    }
    catch(HibernateException e)
    {
        rollback();
    }
    return null;
}
//Non-related methods omitted.
}

Соответствующая конфигурация пружины:

<bean id="profiles" class="com.xxxx.dao.Profiles" />

<bean id="editProfileController" class="com.xxxx.controllers.EditProfileController">
    <property name="sessionForm" value="false" />
    <property name="commandName" value="editProfileCommand" />
    <property name="commandClass" value="com.xxxx.commands.EditProfileCommand" />

    <property name="profiles" ref="profiles" />     

    <property name="formView" value="EditProfile" />
    <property name="successView" value="redirect:/profile" />
    <property name="validator" ref="profileValidator" />
</bean>

hibernate.cfg.xml

<session-factory>
    <property name="connection.driver_class">@driver@</property>
    <property name="connection.url">@connectionurl@</property>
    <property name="connection.username">@dbuser@</property>
    <property name="connection.password">@dbpw@</property>
    <property name="dialect">org.hibernate.dialect.MySQLInnoDBDialect</property>
    <property name="dbcp.maxActive">15</property>
    <property name="dbcp.maxIdle">5</property>
    <property name="dbcp.maxWait">120000</property>
    <property name="dbcp.whenExhaustedAction">2</property>
    <property name="dbcp.testOnBorrow">true</property>
    <property name="dbcp.testOnReturn">true</property>
    <property name="dbcp.validationQuery">
        select 1
    </property>
    <property name="dbcp.ps.maxActive">0</property>
    <property name="dbcp.ps.maxIdle">0</property>
    <property name="dbcp.ps.maxWait">-1</property>
    <property name="dbcp.ps.whenExhaustedAction">2</property>

    <!-- Echo all executed SQL to stdout
    <property name="show_sql">true</property>
    -->

    <mapping resource="com/xxxx/entity/Resource.hbm.xml"/>
    <mapping resource="com/xxxx/entity/Authentication.hbm.xml"/>
    <mapping resource="com/xxxx/entity/NameValuePairs.hbm.xml"/>
    <mapping resource="com/xxxx/entity/Profile.hbm.xml"/>
    <mapping resource="com/xxxx/entity/FileData.hbm.xml"/>
 </session-factory>

Выдержка из Profile.hbm.xml:

<hibernate-mapping>
<class name="com.xxxx.entity.Profile" select-before-update="true">
<id name="id" type="long">
        <generator class="foreign">
            <param name="property">resource</param>
        </generator>
</id>

<set name="primaryRoleSkills" cascade="none">
    <key column="profile"/>
    <many-to-many column="primary_role_skill" class="com.xxxx.entity.PrimaryRoleSkill"/>
</set>
</class>
</hibernate-mapping>

Выдержка из NameValuePairs.hbm.xml:

<hibernate-mapping>
<class name="com.xxxx.entity.NameValuePairs" abstract="true">
    <id name="id" type="long">
    <generator class="native" />
</id>
<discriminator column="type" type="string" />
    <property type="string" name="name" length="256">
        <meta attribute="use-in-equals">true</meta>
    </property>
    <property type="boolean" name="active">
        <meta attribute="default-value">true</meta>
    </property>
    <subclass name="com.xxxx.entity.PrimaryRoleSkill" discriminator-value="PrimaryRoleSkill" />
    </class>
</hibernate-mapping>

Приложение работает на Tomcat 6.0.14 и подключается к сообществу MySQL версии 5.0.89, работающему в Linux. Мы используем Hibernate 3.3.2 и Spring Framework 2.5.6.

Ответы [ 2 ]

1 голос
/ 12 марта 2010

После 10 дней отсутствия исключений я пришел к выводу, что обнаруженное мной решение сработало.

Краткий ответ: я переключил свой DAO на использование HibernateTemplate и использую Spring AOP для обработки транзакций. Это потребовало много переписывания, но оно того стоило, поскольку решение работает так, как задумано. Кроме того, мне не удалось заставить ленивую загрузку работать в моих представлениях JSP, но это не имеет большого значения, поскольку мои объекты довольно малы (я отключил ленивую загрузку свойств в конфигурации Hibernate)

Объяснение: Проблема была в том, как я получал Hibernate Session. В исходной реализации один сеанс Hibernate создается при запуске приложения для каждого DAO, который расширил мой базовый класс DAO. Это вызвало две проблемы. 1) Сеансы Hibernate сами по себе не являются потокобезопасными. Вот почему все тестировалось нормально с одним пользователем на тестовом экземпляре, но при работе было необычно. 2) MySQL любит закрывать соединение через определенный промежуток времени. Поскольку сессии проводились постоянно, это приводило к поломке труб (о них не сообщалось в OP, я думал, что это отдельная проблема) Благодаря этому исправлению Spring теперь управляет созданием / закрытием сеанса, Spring AOP обрабатывает разметку транзакций, а SpringTemplate даже обрабатывает большую часть доступа Hibernate.

1 голос
/ 13 февраля 2010

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

Если бы это был мой код, я бы начал со сравнения реализаций equals (), compareTo () и hashCode () с определениями базы данных - если эти методы сравнивают больше полей, чем требуется для уникальности базы данных, hibernate может рассмотреть два объекты должны отличаться, даже если они используют один и тот же ключ в базе данных.

Другим подходом, который я рассмотрю, было бы добавление регистрации во все места, где эти объекты сохраняются и извлекаются, включая трассировки стека (очевидно, с включением / выключением). Или, в качестве альтернативы, когда вы получаете ошибку дублированного ключа, запросите базу данных и зарегистрируйте то, что уже есть. В любом случае вы хотите узнать, откуда поступает «первая» запись.

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