Avatar billede skindbeni Nybegynder
04. januar 2010 - 00:19 Der er 12 kommentarer

Autoincrement field ved brug af clientdataset

Hej.

Jeg kører Delphi 2007 og bruger Firebird som database.

Jeg har i en del tabeller oprettet et felt ID af typen integer som primær nøgle til at identifisere de enkelte poster.

Jeg har ved samme lejlighed oprettet en generator, der i Firebird kan erstatte den manglende felttype autoinc som flere andre databaser har.

Dette spiller fint, når jeg eksempelvis har en query og den vej igennem læser, retter, tilføjer, slette data til min database.

Jeg er dog i den situation, at jeg i nogle tilfælde vil gøre brug af TClientDataSet til at gemme mine data i buffer under afvikling af applikationen.

Problemet er så, at jeg her får en exception "Field ID must have a value" når jeg forsøger at poste en nyindsat linie.

Det er jeg sådan set enig i, da feltet som primær nøgle ikke må have en null værdi. Generatoren fanger det ikke, måske fordi der ikke kaldes til databasen, men dataene ligger i mit clientdataset indtil jeg kalder applyUpdates.

Er der andre eksperter, der har en løsning eller forslag.
Avatar billede hrc Mester
04. januar 2010 - 00:35 #1
Jeg så en løsning for lang tid siden hvor man i Master-detail konstruktion i CDS lavede negative nøgler som blev erstattet med de rigtige når ApplyChanges blev kørt. Ved ikke om det kan bruges. Skal gerne se om jeg kan finde linket (det ligger på arbejdet).

Alternativt må du bruge CDS'ets OnNewRecord til at kalde generatoren så du får ID'et der. Eneste ulempe med den løsning er at du risikerer huller i rækkefølgen hvis du vælger CancelChanges. Det bør ikke have stor betydning.
Avatar billede arne_v Ekspert
04. januar 2010 - 01:27 #2
Hvis du reelt ikke ønsker at bruge generator, så lad være. Og lad applikationen generere keys. Der er flere forskellige måder at gøre det på: GUID, high-low etc..
Avatar billede pellelil Nybegynder
04. januar 2010 - 08:24 #3
Det jeg har gjort er at jeg bruger en Trigger til at indsætte ID fra en Generator hvis (og kun hvis) dette felt ikke er sat. Desuden har jeg også en StoredProcedure der er i stand til at give mig en værdi fra Generatoren. Denne stored procedure bruger jeg så hvis jeg ønsker at bruge et ClientDataSet til at buffer poster før jeg sender dem videre til databasen

Trigger (Before Insert):
<SNIP>
AS
BEGIN
  /* Set RECID if not allready set */
  if ((new.F_RECID is null) or (new.F_RECID = 0)) then
    new.F_RECID = gen_id(G_SUPPLIERS_RECID, 1);
END
</SNIP>

StoreProcedure:
<SNIP>
CREATE PROCEDURE P_G_SUPPLIERS_RECID (NUMBERS_TO_GENERATE INTEGER)
RETURNS (ID INTEGER)
AS
BEGIN
  ID = Gen_id(G_SUPPLIERS_RECID, NUMBERS_TO_GENERATE);
END
</SNIP>

I dataset'ets "BeforeInsert" Event kan du så kalde den stored proceudre til at hente værdier til generatoren. Alternativt sæt bare værdien på ID feltet til "0" så tager triggeren sig af det.
Avatar billede hrc Mester
04. januar 2010 - 10:59 #4
Mit forslag med at sætte en værdi i OnNewRecord burde kunne fungere sammen med din trigger; den har jo en værdi og vil ikke blive ændret.

arne: Jeg kan ikke se hvorfor man skulle undlade at bruge generatorer. Det virker fint og jeg må antage at kaldet er atomarisk. Især i forhold til at skulle bruge et GUID som nøgle. Det kan ikke undgå at blive langsomt med sådan en nøgle - og bagvedliggende indeks, kan det?
Avatar billede skindbeni Nybegynder
04. januar 2010 - 22:20 #5
Hej.

Det har ikke helt lykkedes for mig. Jeg har taget udgangspunkt i Pellelils lækre kodestumper.

Jeg får stadig samme fejl - "Field ID must have a value"

Jeg har i Firebird:

/* GENERATOR */

CREATE SEQUENCE GEN_POSTERINGER_ID;
ALTER SEQUENCE GEN_POSTERINGER_ID RESTART WITH 0;


/* TRIGGER */

CREATE OR ALTER TRIGGER POSTERINGER_BI FOR POSTERINGER
ACTIVE BEFORE INSERT POSITION 0
AS
BEGIN
  IF ((NEW.ID IS NULL) or (NEW.ID = 0)) THEN
    NEW.ID = GEN_ID(GEN_POSTERINGER_ID,1);
END


/* STORED PROCEDURE */

CREATE OR ALTER PROCEDURE POSTERINGER_RECID (
    numbers_to_generate integer)
returns (
    id integer)
as
begin
  /* Procedure Text */
  ID = Gen_id(GEN_POSTERINGER_ID, NUMBERS_TO_GENERATE);
  suspend;
end^


I Delphi har jeg udover mine data aware komponenter samt min database, IBquery, datasetprovider, clientdataset og datasource også tilføjet en IBStoredProc.

I såvel clientdatasetets BeforeInsert og BeforePost events har jeg forsøgt med at kalde den ovenstående oprettede procedure via IBStoredProc.

Jeg får ingen "direkte" fejlmeddelelse på denne del af koden, men jeg får stadig fejlmeddelelse om, at ID ikke er fyldt ud.
Avatar billede arne_v Ekspert
05. januar 2010 - 03:00 #6
hrc>

Jeg synes at det er helt fint at håndtere det i databasen altid.

Jeg synes at det er helt fint at håndtere det i applikationen altid.

Jeg synes at det er noget rod at lave det både i database og applikation med en arbejdsfordeling som afhænger lidt af hvad det er man laver.
Avatar billede hrc Mester
05. januar 2010 - 10:56 #7
skindbeni: Har du prøvet at sætte værdien i CDS'ets OnNewRecord? Sådan plejer vi at gøre det (via et kald til en SProc der tæller en generator op). Kan det eventuelt være andre felter i recorden du mangler at udfylde?

arne: Tingene skal køre det ene eller det andet sted. Det er jeg helt med på - det var bare ikke det jeg spurgte om. Mit spørgsmål gik på om ikke GUID som primærnøgle var en sløv og uhensigtsmæssig løsning.
Avatar billede skindbeni Nybegynder
05. januar 2010 - 13:21 #8
@hrc

Det giver desværre ingen ændring.

Det er selvfølgelig ingen tvivl om, at jeg et eller andet sted har lavet en fejl eller mangler noget.

Nu er det iøvrigt mit allerførste bekendtskab med en "stored procedure" og "trigger" og det er muligvis der, det løber løbsk formig.

Når jeg kalder min StoredProc, så bliver variablen ID tilføjet en værdi, som jeg tolker proceduren.

Som jeg har gjort det i min kode indtil nu, så har jeg udelukkende nedenståënde kode, som jeg har forsøgt i såvel CDS'ens BeforeInsert, BeforePost og nu OnNewRecord:

  IBStoredProc1.ParamByName('Numbers_To_Generate').Value := 0;
  IBStoredProc1.ExecProc;

Den første linie eller noget lignende har jeg fundet ud er en nødvendighed. Fjerner jeg den får jeg nedenstående fejl, når jeg forsøger at indsætte en ny linie:

"Required Param Value not set"

Tilbage til emnet:

Har jeg ikke ret i, at jeg mangler en linie ala 3. linie, som er den nye linie jeg har indsat i OnNewRecord eventen.

  IBStoredProc1.ParamByName('Numbers_To_Generate').Value := 0;
  IBStoredProc1.ExecProc;
  cdsKassekladde.FieldByName('ID').AsInteger := IBSToredProc1.ParamByName('ID').AsInteger;

Er det korrekt anvendelse? Det løser i hvert fald problemet, men genererer et nyt problem.

Når jeg så forsøger at poste (ikke indsætte) en linie 2 i min grid (eller for den sags skyld lukker applikationen og starter op igen og forsøger at indlæse poste første linie), så får jeg en "key violation" exception.

Jeg har kigget i min database, og generatoren har IKKE talt 1 værdi op. Det giver, at der må være noget i min trigger, generator eller storedProc, der ikke spiller. Igen er det mit første bekendtskab med disse og jeg har svært ved at gennemskue, hvori fejlen evt. ligger.
Avatar billede arne_v Ekspert
06. januar 2010 - 03:39 #9
hrc>

Min efterhpånden lidt ældre PC kan generere 200000 GUID'er i sekundet.

Det svarer til 12 millioner GUID'er i minuttet.

Testet med Delphi 7 på Windows XP SP3.
Avatar billede hrc Mester
06. januar 2010 - 09:48 #10
arne: Tjaa, det lyder jo meget godt (har du SSD-disk? :-)) men 16 bytes pr nøgle bliver til en del i et indeks. Har du prøve at oprette tilsvarende for int-nøgler (med og uden identity)?

Som Chad skriver (http://social.msdn.microsoft.com/Forums/en/sqltools/thread/2c4890c7-cf21-47b2-a765-c5017297f269), så er GUID'er som PK' altid "up for debate". Ved database-replikering har M$ valgt typen som nøgle. Må derfor antage, at indekserne er optimeret dertil. Kan godt lide tanken om at den er globalt unik (nationalt ville nu være OK for mig).

Læste i øvrigt at uniqueidentifier også smækker det først fundne netkorts MAC-adresse i enden af GUID'et. Det gør også lige nøglen lidt større.
Avatar billede arne_v Ekspert
06. januar 2010 - 14:50 #11
Jeg bruger ikke databasen til at generere GUID - jeg genererer de GUID's i Delphi - min lille test brugte slet ikke noget database overhovedet.

Jeg kan ikke forestille mig at index paa GUID skulle vaere langsommere end index paa saa mange andre typer felter.

Natural keys versus artificial keys er altid debatteret.

Og til en vis grad ogsaa GUID versus auto increment versus anden teknik.
Avatar billede skindbeni Nybegynder
07. januar 2010 - 22:24 #12
Hej igen.

Ingen der har et bud på, hvorfor jeg får "Key Violation" fejl, når jeg indsætter en (ny) linie.

Det er som om generatoren ikke registerer, at den skal tælle 1 op?
Avatar billede Ny bruger Nybegynder

Din løsning...

Tilladte BB-code-tags: [b]fed[/b] [i]kursiv[/i] [u]understreget[/u] Web- og emailadresser omdannes automatisk til links. Der sættes "nofollow" på alle links.

Loading billede Opret Preview

Log ind eller opret profil

Hov!

For at kunne deltage på Computerworld Eksperten skal du være logget ind.

Det er heldigvis nemt at oprette en bruger: Det tager to minutter og du kan vælge at bruge enten e-mail, Facebook eller Google som login.

Du kan også logge ind via nedenstående tjenester