Bien qu'il s'agisse d'un ancien fil de discussion, je souhaite partager ma solution et j'espère obtenir des commentaires à ce sujet. Soyez averti que je n'ai testé cette solution qu'avec ma base de données locale dans certains cas de test JUnit. Ce n'est donc pas une fonctionnalité productive jusqu'à présent.
J'ai résolu ce problème pour mon en introduisant une annotation personnalisée appelée Séquence sans propriété. C'est juste un marqueur pour les champs auxquels une valeur doit être attribuée à partir d'une séquence incrémentée.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Sequence
{
}
En utilisant cette annotation, j'ai marqué mes entités.
public class Area extends BaseEntity implements ClientAware, IssuerAware
{
@Column(name = "areaNumber", updatable = false)
@Sequence
private Integer areaNumber;
....
}
Pour garder les choses indépendantes de la base de données, j'ai introduit une entité appelée SequenceNumber qui contient la valeur actuelle de la séquence et la taille de l'incrément. J'ai choisi le className comme clé unique pour que chaque classe d'entité obtienne sa propre séquence.
@Entity
@Table(name = "SequenceNumber", uniqueConstraints = { @UniqueConstraint(columnNames = { "className" }) })
public class SequenceNumber
{
@Id
@Column(name = "className", updatable = false)
private String className;
@Column(name = "nextValue")
private Integer nextValue = 1;
@Column(name = "incrementValue")
private Integer incrementValue = 10;
... some getters and setters ....
}
La dernière étape et la plus difficile est un PreInsertListener qui gère l'attribution des numéros de séquence. Notez que j'ai utilisé le ressort comme récipient à grains.
@Component
public class SequenceListener implements PreInsertEventListener
{
private static final long serialVersionUID = 7946581162328559098L;
private final static Logger log = Logger.getLogger(SequenceListener.class);
@Autowired
private SessionFactoryImplementor sessionFactoryImpl;
private final Map<String, CacheEntry> cache = new HashMap<>();
@PostConstruct
public void selfRegister()
{
// As you might expect, an EventListenerRegistry is the place with which event listeners are registered
// It is a service so we look it up using the service registry
final EventListenerRegistry eventListenerRegistry = sessionFactoryImpl.getServiceRegistry().getService(EventListenerRegistry.class);
// add the listener to the end of the listener chain
eventListenerRegistry.appendListeners(EventType.PRE_INSERT, this);
}
@Override
public boolean onPreInsert(PreInsertEvent p_event)
{
updateSequenceValue(p_event.getEntity(), p_event.getState(), p_event.getPersister().getPropertyNames());
return false;
}
private void updateSequenceValue(Object p_entity, Object[] p_state, String[] p_propertyNames)
{
try
{
List<Field> fields = ReflectUtil.getFields(p_entity.getClass(), null, Sequence.class);
if (!fields.isEmpty())
{
if (log.isDebugEnabled())
{
log.debug("Intercepted custom sequence entity.");
}
for (Field field : fields)
{
Integer value = getSequenceNumber(p_entity.getClass().getName());
field.setAccessible(true);
field.set(p_entity, value);
setPropertyState(p_state, p_propertyNames, field.getName(), value);
if (log.isDebugEnabled())
{
LogMF.debug(log, "Set {0} property to {1}.", new Object[] { field, value });
}
}
}
}
catch (Exception e)
{
log.error("Failed to set sequence property.", e);
}
}
private Integer getSequenceNumber(String p_className)
{
synchronized (cache)
{
CacheEntry current = cache.get(p_className);
// not in cache yet => load from database
if ((current == null) || current.isEmpty())
{
boolean insert = false;
StatelessSession session = sessionFactoryImpl.openStatelessSession();
session.beginTransaction();
SequenceNumber sequenceNumber = (SequenceNumber) session.get(SequenceNumber.class, p_className);
// not in database yet => create new sequence
if (sequenceNumber == null)
{
sequenceNumber = new SequenceNumber();
sequenceNumber.setClassName(p_className);
insert = true;
}
current = new CacheEntry(sequenceNumber.getNextValue() + sequenceNumber.getIncrementValue(), sequenceNumber.getNextValue());
cache.put(p_className, current);
sequenceNumber.setNextValue(sequenceNumber.getNextValue() + sequenceNumber.getIncrementValue());
if (insert)
{
session.insert(sequenceNumber);
}
else
{
session.update(sequenceNumber);
}
session.getTransaction().commit();
session.close();
}
return current.next();
}
}
private void setPropertyState(Object[] propertyStates, String[] propertyNames, String propertyName, Object propertyState)
{
for (int i = 0; i < propertyNames.length; i++)
{
if (propertyName.equals(propertyNames[i]))
{
propertyStates[i] = propertyState;
return;
}
}
}
private static class CacheEntry
{
private int current;
private final int limit;
public CacheEntry(final int p_limit, final int p_current)
{
current = p_current;
limit = p_limit;
}
public Integer next()
{
return current++;
}
public boolean isEmpty()
{
return current >= limit;
}
}
}
Comme vous pouvez le voir à partir du code ci-dessus, l'auditeur a utilisé une instance SequenceNumber par classe d'entité et réserve un couple de numéros de séquence définis par incrementValue de l'entité SequenceNumber. S'il manque de numéros de séquence, il charge l'entité SequenceNumber pour la classe cible et réserve les valeurs incrementValue pour les appels suivants. De cette façon, je n'ai pas besoin d'interroger la base de données chaque fois qu'une valeur de séquence est nécessaire. Notez la StatelessSession qui est en cours d'ouverture pour réserver le prochain ensemble de numéros de séquence. Vous ne pouvez pas utiliser la même session que l'entité cible est actuellement persistante car cela entraînerait une ConcurrentModificationException dans EntityPersister.
J'espère que cela aide quelqu'un.