/** * saving and cloning records * * @param <T> POJOs must extend DefaultRecord * @param recordClass class of the records to clone * @param records records to clone * @return list of cloned and saved records */ public <T extends Record> List<T> clone(Class<T> recordClass, T... records) { RecordWrapper cloneWrapper = new RecordWrapper(); List<T> list = new ArrayList<>(); mc.transaction( () -> { // saving records save(records); // cloning for (T record : records) { RecordWrapper wrapper = mc.getTransactionEntityManager().find(RecordWrapper.class, record.getId()); cloneWrapper.setRecordType(wrapper.getRecordType()); cloneWrapper.setTenant(wrapper.getTenant()); if (wrapper.getData() != null) { cloneWrapper.setData(wrapper.getData()); } //// if (wrapper.getDocument() != null) { //// cloneWrapper.setDocument(wrapper.getDocument()); //// } // performing database operations mc.getTransactionEntityManager().persist(cloneWrapper); T recordClone = mc.getRecord(recordClass, wrapper); indexRecord(true, recordClone); auditor.logCreateRecord(cloneWrapper); list.add(recordClone); } }); return list; }
/** * listing "distinct" and sorted values from an index key * * @param key index key to query as defined in the FieldIndexing annotation (if not empty) * @return list of string of available values for the index */ public List<String> getIndexList(String key) { List selectedList = mc.getTransactionEntityManager() .createNamedQuery("RecordIndex.findValueFromKey") .setParameter("key", key) .getResultList(); if (selectedList.isEmpty()) { selectedList = mc.getTransactionEntityManager() .createNamedQuery("RecordIndex.findDateFromKey") .setParameter("key", key) .getResultList(); } if (selectedList.isEmpty()) { selectedList = mc.getTransactionEntityManager() .createNamedQuery("RecordIndex.findNumericFromKey") .setParameter("key", key) .getResultList(); } List<String> list = new ArrayList(); for (Object o : selectedList) { list.add(o.toString()); } return list; }
/** * Saving records to the database. JPA handles bath writing, so parameter paging can be ignored * (vs remove). * * @param <T> POJOs must extend DefaultRecord * @param list list of records to save in the database */ public <T extends Record> void save(List<T> list) { if (list != null) { mc.transaction( () -> { list.stream() .filter((record) -> (record != null)) .forEach( (record) -> { Long recordId = record.getId(); if (recordId == null) { // persisting new record RecordWrapper wrapper = createRecordWrapper(record); indexRecord(true, record); auditor.logCreateRecord(wrapper); } else { // using reference as only SET is necesssary RecordWrapper wrapper = mc.getTransactionEntityManager() .getReference( RecordWrapper.class, recordId); // getReference creating two queries including // RecordType wrapper.setData(mc.toWrapper(record)); //// if (record.isDocumentChanged()) { //// wrapper.setDocument(record.getDocument()); //// } indexRecord(false, record); auditor.logUpdateRecord(wrapper, false); // //record.isDocumentChanged()); } }); }); } }
/** * Gives the type of a record from its id. Main purpose is to check if the manipulated id has the * correct type. * * @param id record id * @return type name of the record */ public String getRecordType(Long id) { RecordWrapper record = mc.getTransactionEntityManager().find(RecordWrapper.class, id); if (record != null) { return mc.getTransactionEntityManager() .find(RecordType.class, record.getRecordType()) .getCode(); } return null; }
/** * deleting records from the database * * @param <T> POJOs must extend DefaultRecord * @param records list of records to remove from the database */ public <T extends Record> void remove(List<T> records) { if (!records.isEmpty()) { // -- removing records by max batch of PARAMETER_PAGING which is the max number of paramaters // supported by some databases (ex PostgreSQL) // -- single transaction for all deletes to preserve integrity and faster processing (batch // SQL). mc.transaction( () -> { int start = 0; int end = Math.min(records.size(), PARAMETER_PAGING); while (start < records.size()) { removeTransaction(records.subList(start, end)); start = end; end = Math.min(records.size(), end + PARAMETER_PAGING); } }); // -- if transaction is successfull unvalidating IDs records .stream() .forEach( (record) -> { record.setId(-1L); }); } }
/** * parsing record to extract indexed fields * * @param isNew record has never been parsed * @param record record to parse */ private void indexRecord(boolean isNew, Record record) { for (Field field : mc.getIndexingField(record.getClass())) { try { // indexes can be shared by providing common key, or default key using field path is used String key = field.getAnnotation(FieldIndexing.class).value(); String name = field.getName(); if (key.length() == 0) { key = record.getClass().getName() + "." + name; } // reading field value to add to index list boolean status = field.isAccessible(); field.setAccessible(true); Object fieldValue = field.get(record); field.setAccessible(status); // database access EntityManager em = mc.getTransactionEntityManager(); RecordIndexPK pk = new RecordIndexPK(key, record.getId(), name); // forcing indexe deletion to preserve record batch save (npreveting select for each save) // Note : make sure the ModelController provides an entity manager with // setShouldPerformDeletesFirst(true) if (!isNew) { em.remove(em.getReference(RecordIndex.class, pk)); } // mapping the index to the database Comparable converted = pm.getConverted(fieldValue); if (converted != null) { RecordIndex ri = new RecordIndex(pk); switch (pm.getType(field.getType())) { case DATE: ri.setDate((Date) converted); break; case STRING: ri.setValue((String) converted); break; case NUMERIC: ri.setNumeric((BigDecimal) converted); break; } // saving em.persist(ri); } } catch (IllegalArgumentException | IllegalAccessException ex) { // convet to runtime exception throw new RuntimeException(ex); } } }
/** * creating a new record * * @param <T> POJOs must extend DefaultRecord * @param record record to save * @return the entity encapsulating the record in the database */ private <T extends Record> RecordWrapper createRecordWrapper(T record) { RecordWrapper wrapper = new RecordWrapper(); RecordType recordType = mc.getType(record.getClass(), false); wrapper.setRecordType(recordType.getId()); wrapper.setData(mc.toWrapper(record)); //// if (record.isDocumentChanged()) { //// wrapper.setDocument(record.getDocument()); //// } // -- tenant management Object tenant = mc.getTransactionEntityManager() .getProperties() .get(EntityManagerProperties.MULTITENANT_PROPERTY_DEFAULT); if (tenant != null) { wrapper.setTenant(tenant.toString()); } // -- getting the id from the JPA mc.getTransactionEntityManager().persist(wrapper); Long recordId = wrapper.getId(); record.setId(recordId); return wrapper; }
/** * removing parents and children * * @param parentId record id * @param childId record id */ private void removePath(Long parentId, Long childId) { EntityManager em = mc.getTransactionEntityManager(); // -- building branch to remove from below hierarchy >> using parent to use only this parent // path if more than one exists List<Long> pathList = em.createQuery( "SELECT rp.recordPK.path FROM RecordPath rp WHERE rp.recordPK.child = :child") .setParameter("child", parentId) .getResultList(); pathList.add(parentId); // -- retrieving hierarchies to complete with new branch >> finding all children of reference List<RecordPathPK> pkList = em.createQuery("SELECT rp.recordPK FROM RecordPath rp WHERE rp.recordPK.path = :path") .setParameter("path", childId) .getResultList(); pkList.add(new RecordPathPK(parentId, childId, parentId)); // -- applying path branch to childrens for (RecordPathPK pk : pkList) { for (Long path : pathList) { pk.setPath(path); em.remove(em.getReference(RecordPath.class, pk)); } } }
/** * Spawn transaction to group multiple writing operations. Otherwise a transaction is * encapsulating each operation. * * @param runnable for Lambda */ public void transaction(Runnable runnable) { mc.transaction(runnable); }
/** * Logic removing the record as well as the links. Multiple trials are preserved in comments just * for reminder on current reference implementation. * * @param <T> POJOs must extend DefaultRecord * @param recordList list of records to remove */ private <T extends Record> void removeTransaction(List<T> recordList) { // String recordType = records.get(0).getClass().getName(); // -- extracting IDs for batch processing ArrayList<Long> idList = new ArrayList<>(); for (Record record : recordList) { idList.add(record.getId()); // -- POSTGRES won't perform correctly with multiple delete query // mc.getEntityManager().createNamedQuery("RecordLink.deleteByRecordId").setParameter("record", record.getId()).executeUpdate(); // mc.getEntityManager().createNamedQuery("RecordWrapper.deleteByRecordId").setParameter("record", record.getId()).executeUpdate(); // record.setId(null); // ------------------------------------------------------------------ } // -- DELETE query less efficient than JPA remove (= batch) by x3 // mc.getEntityManager().createNamedQuery("RecordLink.deleteByRecordIdList").setParameter("list", idList).executeUpdate(); // mc.getEntityManager().createNamedQuery("RecordWrapper.deleteByRecordIdList").setParameter("list", idList).executeUpdate(); // ---------------------------------------------------------------------- EntityManager em = mc.getTransactionEntityManager(); // -- removing links // -- two queries to fetch links is more efficient than a single with OR by x10 // -- CursoredStream is effective in memory management and helps to get better performances than // EAGER fetch CursoredStream pkList1 = (CursoredStream) em.createNamedQuery("RecordLink.findByReferenceIdList") .setParameter("list", idList) .setHint("eclipselink.cursor", true) .getSingleResult(); CursoredStream pkList2 = (CursoredStream) em.createNamedQuery("RecordLink.findByLinkIdList") .setParameter("list", idList) .setHint("eclipselink.cursor", true) .getSingleResult(); // -- using reference to avoid complete loading of obsolete objects while (!pkList1.atEnd()) { for (Object pkO : pkList1.next(100)) { RecordLinkPK pk = (RecordLinkPK) pkO; em.remove(em.getReference(RecordLink.class, pk)); auditor.logRemoveLink(pk.getReference(), pk.getLink()); } } pkList1.close(); while (!pkList2.atEnd()) { for (Object pkO : pkList2.next(100)) { RecordLinkPK pk = (RecordLinkPK) pkO; em.remove(em.getReference(RecordLink.class, pk)); // no log here as duplicated } } pkList2.close(); // -- removing paths List<RecordPathPK> childList = em.createNamedQuery("RecordPath.findChildFromIdList") .setParameter("list", idList) .getResultList(); for (RecordPathPK path : childList) { removePath(path.getParent(), path.getChild()); auditor.logRemovePath(path.getParent(), path.getChild()); } List<RecordPathPK> parentList = em.createNamedQuery("RecordPath.findParentFromIdList") .setParameter("list", idList) .getResultList(); for (RecordPathPK path : parentList) { removePath(path.getParent(), path.getChild()); auditor.logRemovePath(path.getParent(), path.getChild()); } // -- removing indexes List<RecordIndexPK> indexList = em.createNamedQuery("RecordIndex.findIndexFromIdList") .setParameter("list", idList) .getResultList(); indexList .stream() .forEach( (index) -> { em.remove(em.getReference(RecordIndex.class, index)); }); // -- removing records for (Long id : idList) { RecordWrapper rw = em.getReference(RecordWrapper.class, id); em.remove(rw); auditor.logRemoveRecord(rw); } }