Ordinamento tabella dettaglio

Buongiorno,
classico esempio di un documento master/detail. Perche’ quando inserisco le righe di dettaglio, salvo il documento e lo riapro le righe hanno un ordinamento diverso da quello di inserimento?
Sono riuscito ad impostare da codice l’ordinamento su una colonna, ad esempio sul codice articolo o sulla descrizione, ma vorrei fare l’ordinamento per ordine di inserimento e non riesco proprio a capire come fare.
E’ possibile farlo?
Grazie
Marco

Ciao Marco,

hai provato ad ordinare sul campo di sistema CreateTs. Nella query JQL dovresti scrivere qualcosa del genere:

select e from tua_entita e orderby e.createTs

Ciao

Ciao, grazie per la risposta.
La tua soluzione era la prima cosa che ho pensato, ma si tratta di ordinare la tabella del dettaglio, la query non e’ visibile, credo la componga al volo Cuba in base al master.
L’ordinamento della tabella ci sarebbe, ma non riesco a rendere visibili le colonne di sistema, quindi posso ordinare solo su le colonne che ho creato io nell’entità, il resto c’e’, ma non lo vedo.
Se si riuscisse a rendere visibile createTs potrebbe essere la soluzione.
Marco

Sono riuscito a fare l’ordinamento sul dettaglio per createTs come suggerito da mnemonik83, a questo punto mi sono reso conto che il problema e’ direttamente nel db, i record sono salvati con un ordinamento che non riesco a capire, forse per uuid, quindi anche il timestamp non e’ utilizzabile…
Rimango in attesa di suggerimenti prima di gestire un numeratore di riga :frowning:
Marco

Ciao Marco!

Prima di tutto una spiegazione di cosa sta accadendo dietro le quinte: PostgreSQL non ha un concetto di “clustered index” come quelli in MSSQL o MySQL, cioè non garantisce l’ordine fisico di inserimento dei record in una tabella.
Pertanto l’ordine di inserimento (detto anche “naturale”) non è una cosa su cui si può fare affidamento, e in linea generale non si dovrebbe mai farci affidamento anche in altri RDBMS. E’ vero che può essere uno shock se arrivi da MySQL che crea in automatico (e senza possibilità di disattivare) un indice cluster sulla PK, o da MSSQL dove di default i DBA superficiali creano indici cluster sulla PK (ma si può disattivare per fortuna).

Premesso questo, la soluzione corretta è come ha detto @mnemonik83 di specificare l’ordine di default delle righe per createTs, e hai due modi per farlo, uno veloce ma che di solito non uso, e uno che invece ti fa lavorare “bene” ma significa scrivere codice in più e non usare quello generato in automatico:

OPZIONE 1 - usa l’annotazione @OrderBy sulla relazione uno a molti:

    @OneToMany(mappedBy = "gruppo")
    @NotNull
    @OrderBy("pippo")
    private List<GruppoProdottiProdotto> prodotti = Collections.emptyList();

Screenshot 2020-12-02 at 11.50.18

OPZIONE 2 - non usare l’attributo List per navigare la relazione, cioè usando una sub-collection dentro la Instance che usi in edit. Invece crei una seconda collection separata sulla entità collegata, la colleghi con il facet dataLoadCoordinator alla master Instance e usi quella collection sulla Table/Grid collegata:

....
    <data>
        <instance id="fornitoreDc"
                  class="com.company.entity.Fornitore">
            <view extends="_local">
                <property name="logo" view="_minimal"/>
            </view>
            <loader/>
        </instance>
        <collection id="produttoriDc" class="com.company.FornitoreProduttore" view="_minimal">
            <loader id="produttoriDl">
                <query>
                    <![CDATA[select e from app_FornitoreProduttore e where e.fornitore = :container_fornitoreDc]]>
                </query>
            </loader>
        </collection>
    </data>
    <dialogMode height="600"
                width="800"/>
    <facets>
        <dataLoadCoordinator auto="true"/>
    </facets>

....

Ovviamente richiede di gestire un po’ di cose in più a mano (ma mica tante), ma d’altronde dovrai arrivarci per forza, perché nella mia esperienza si arriva solo fino a un certo punto con gli automatismi.

Cmq puoi iniziare dalla prima soluzione per risolvere veloce.

A presto,
Paolo

Ciao Paolo,
intanto grazie per la spiegazione chiarissima come al solito. Ero molto vicino alla soluzione, solo stavo guardando nel posto sbagliato, guardavo nell’entita’ del dettaglio invece che nel master. Un “disegno” aiuta sempre :slight_smile:

Il problema dell’inserimento delle righe nella tabella in ordine “casuale” rimane, anche annotando l’entità con @OrderBy, l’inserimento in db delle righe lo fa comunque nell’ordine che gli pare, il ts va di conseguenza, il successivo ordinamento dovuto all’annotazione funziona, ma non risolve il problema.

Quindi se io voglio salvare e rivedere una tabella nell’ordine esatto di inserimento delle righe fatto dall’utente e’ impossibile farlo senza un contatore di riga o sono io che come al solito non ho capito qualcosa?

Saluti e grazie della pazienza

Marco

L’ordine esatto di inserimento è appunto order by createTs e lo sarà sempre (o corrispondente SQL).

Il motore database non garantisce l’ordine fisico dei dati nelle sue pagine, non lo fa nessuno, che sia Postgres, MSSQL, MySQL o altri che ti possono venire in mente :wink:

Cosa dà l’illusione dell’ordine fisico in altri motori di RDBMS? Un indice cosiddetto clustered, che non esiste in Postgres (o meglio non nell’accezione degli altri motori, se vuoi approfondire cosa si intende per clustered in PG ti rimando ai docs tecnici di Postgres).

Un indice clustered in MySQL o MSSQL fa in modo che i dati nelle pagine fisiche siano sempre mantenuti ordinati fisicamente ogni volta che si eseguono istruzioni DML di modifica (INSERT, UPDATE, DELETE e altre operazioni interne di maintenance).
In MSSQL è possibile creare da 0 a N indici clustered su una o più colonne di una tabella, in MySQL (un motore che ahimè soffre di una architettura nata male e cresciuta sul male) invece si può avere un solo indice clustered di default, e questo è esattamente quello sulla PK (scelta orribile).

Che problema ha un indice clustered? Semplice, dato che ogni operazione (esterna o interna) che modifica i dati delle colonne di un indice clustered scatena il riordino fisico di TUTTI i record della tabella, soffrono di problemi di performance in caso di tabelle di grandi dimensioni, o con grande frequenza di modifica.
Inoltre sono la morte totale in caso di indici basati su dati altamente casuali, quindi ad esempio il peggiore di tutti, l’UUID o un grande varchar. E’ altamente sconsigliato avere un indice clustered su questi tipi di campi. (quindi in MySQL mai avere una PK su campi a grande variabilità, come un varchar che contiene un UUID, default di CUBA e StandardEntity ad esempio)

Quindi per concludere, potresti avere l’ordinamento fisico su un motore come MSSQL, garantito se e solo se esiste un indice clustered sulla/e colonna/e su cui vuoi che sia garantito tale ordine. Non è possibile su PostgreSQL, mentre è limitato sulla sola PK in MySQL.
Ma al netto di questo, nessun motore DB garantisce l’ordine naturale di inserimento dei record, perché ad esempio qualunque operazione interna del motore di ottimizzazione delle strutture dati potrebbe cambiare questo ordine da un momento all’altro.

Spero che questo lungo sproliloquio ti abbia chiarito il perché devi fare affidamento SOLO ad ORDER BY per ottenere l’ordine desiderato dei dati nelle tue query :wink:

Paolo

Dimenticavo:

E’ impossibile ANCHE con un contatore di riga… ti servirà sempre order by ilMioContatore se vuoi ordinare per esso, ma a questo punto hai già order by createTs quindi lo trovo inutile :wink:

Paolo

Ciao Paolo,
il concetto dell’ordinamento mi e’ ben chiaro.

Per contatore intendevo un attributo tipo numerico aggiunto dell’entità dettaglio, gestito da codice (lo so che e’ una cosa orrenda), qualcosa tipo quando inserisco il record guardo quante righe ci sono nella collezione del dettaglio al momento e aggiungo uno…quando salva lo fa come gli pare, ma io ho messo l’ordinamento sul campo contatore quindi quando recupero i record ottengo sempre la stessa sequenza.
Se cancello delle righe e ci sono dei buchi non e’ importante, la sequenza rimane.

Che ne pensi?

Marco

E’ la stessa cosa che usare createTs non cambia nulla. Il timestamp è sempre valorizzato, se cancelli una riga l’ordine rimane lo stesso. Un timestamp avrà lo stesso ordinamento di un contatore.
Cosa non ti piace del createTs a parte essere di sistema?

P.

Niente, il createTs era la soluzione ideale, e’ la prima cosa che ho provato su suggerimento di mnemonik, ma il createTs segue l’ordinamento di inserimento dei record nel db, quindi sul db non ho lo stesso ordinamento che l’utente vede a video prima di premere salva.

Praticamente io ho due righe inserite dall’utente articolo A riga 1, articolo B riga 2, premo salva, riapro il documento e mi posso trovare articolo A riga 2, articolo B riga 1. Pensa un po’ se di righe ce ne sono 100, il povero utente come fa a ricontrollare se sta copiando da un elenco che ha in mano?

Marco

Quando usi gli automatismi di CUBA, in particolare la COMPOSITION, non hai il controllo sull’ordine delle insert sulla singola commit (transaction) che viene fatta sul DB. Non è un problema risolvibile se usi gli automatismi, lo è se gestisci a mano.
Quindi CUBA stesso quando fa la transazione potrebbe fare:

insert articolo C
insert articolo A
insert articolo B

Non è garantito l’ordine delle operazioni all’interno del commit, ma anche questo è nornale. In pratica il createTs segue l’ordine delle insert, cioè C, A e B.

Quindi sì, in questo caso se non vuoi gestire a mano la creazione dei detail (cosa che faccio di solito), se vuoi mantenere l’ordine di inserimento devi usare un contatore di articoli, e ordinare per quello.

Quando leggi i dati e anche quando lavori con tabelle a video con dati in memoria, devi stabilire sempre tu l’ordine corretto dei record.
Per la lettura la prima volta che riapri una schermata dopo salvataggio userai o l’order by nella JPQL corrispondente, oppure nella table con attributo sort:

<columns>
    <column property="name" sort="ASCENDING"/>
</columns>

Il secondo approccio è meglio nel tuo caso perché rimane ordinata la tabella su quella colonna anche se la aggiorni, mentre con order by nella JPQL i dati vengono ordinati solo al primo caricamento e poi inseriti nella List della data collection (in memoria) con quell’ordine.

P.

Nota: l’approccio del contatore gestito in memoria e salvato al commit funziona solo su un approccio di pessimistic locking in caso di sistema multi utente. Cioè se due o più utenti lavorano sullo stesso ordine (master) in edit, il primo che salva avrà i record corretti, il secondo e poi gli altri faranno casino perché hai generato i contatori in memoria senza sapere che altri N utenti ci stavano lavorando…
Devi:

  • aggiungere un constraint univoco su id master+contatore detail
  • abilitare il lock esclusivo sul record ordine per bloccare la schermata e renderla read-only per gli utenti successivi al primo

Se invece vuoi un vero multi user, devi gestire la generazione del contatore in due fasi, una temporanea mentre sei in edit, e una in fase di commit. Ma a questo punto diventerebbe più complesso rispetto a non usare gli automatismi di cuba della composition, quindi meglio il lock che ti ho scritto sopra.

P.

Hai ragione, se due utenti lavorano sullo stesso documento viene fuori un casino…
Per lo scopo “didattico” dell’applicazione che sto tirando su penso si possa applicare anche il lock pessimistico direttamente testata dei documenti, non c’e’ necessità di modificare contemporaneamente lo stesso documento da parte di più utenti.
Provo ad implementare il contatore di riga.
Grazie di tutto come sempre
Marco

Salve a tutti,
ho risolto mettendo il contatore di riga, rimane il problema della multiutenza, ma credo sia accettabile su documenti amministrativi tipo fatture, ddt, ordini, ecc il lock pessimistico sul master.
Buon we
Marco

Ciao,

intervengo, forse dicendo una banalità, ma ci provo ugualmente.
Puoi provare a gestire le insert in transazioni differenti e non una unica, in modo che il timestamp risulterà sempre corretto. In pratica ti crei un beforeCommit nel quale cicli sugli item della collezione che ottieni dal COMPOSITION e per ogni item fai un commit tramite un service.

Ti riporto un esempio:

Questo mi serve pre prendere l’oggetto creato nello screen dell’entità del COMPOSITION:

@Subscribe("companyRelationStatesTable.create")
    protected void onCompanyRelationStatesCreate(Action.ActionPerformedEvent event) {
        PersonCompanyRelationStateEdit personCompanyRelationStatesEdit = screenBuilders.editor(PersonCompanyRelationState.class, this)
                .withScreenClass(PersonCompanyRelationStateEdit.class)
                .editEntity(new PersonCompanyRelationState())
                .withParentDataContext(dataContext)
                .withOptions(new MapScreenOptions(ParamsMap.of("person", getEditedEntity())))
                .withAfterCloseListener(personCompanyRelationStateEditAfterScreenCloseEvent -> {
                    if (personCompanyRelationStateEditAfterScreenCloseEvent.closedWith(StandardOutcome.COMMIT)) {
                        companyRelationStatesDc.getMutableItems().add((PersonCompanyRelationState) personCompanyRelationStateEditAfterScreenCloseEvent.getSource().getEditedEntity());
                    }
                })
                .build();
        personCompanyRelationStatesEdit.show();
    }

poi in un beforeCommit fai:

@Subscribe
    protected void onBeforeCommitChanges(BeforeCommitChangesEvent event) {
        
        for (int i=0; i<companyRelationStatesDc.getMutableItems().size(); i++) {
            myService.commit(companyRelationsDc.getMutableItems().get(i));
        }
        companyRelationsDc.getMutableItems().clear();
    }

E’ da provare, in teoria dovrebbe reggere.

Ciao

@mnemonik83 si sicuramente funziona, ma a questo punto tanto vale non usare la COMPOSITION, che è quello che faccio io da sempre.
La composition non fa altro che impostare il parent DataContext per le schermate di edit figlie di una relazione. E quando imposti il parent DataContext in una schermata di edit, CUBA non fa il commit alla chiusura dello screen, ma salva le entità create/modificate nel DataContext del parent. Quando poi salvi il parent committa tutto di botto. Qui trovi il doc a riguardo:
https://doc.cuba-platform.com/manual-7.2/gui_DataContext.html#parent_data_context
In pratica la composition è poco più di quello che trovi nel paragrafetto di cui sopra…

Visto che a me tutti sti automatismi mi garbano poco, e sinceramente trovo fuoviante la distinzione tra associazione e composizione, preferisco avere il controllo di quello che faccio in ogni momento :wink:

Paolo