dbsys.info

Migregring av Hobbyhuset til Neo4j

Denne nettsiden inneholder forslag til skript/kode for å flytte/migrere eksempeldatabasen Hobbyhuset fra MySQL til Neo4j. Ikke alt er testet like godt – noe kan gi feil og mye kan sikkert gjøres på andre måter. Tips til forbedringer mottas!

En relasjonsdatabase kan importeres i Neo4j på flere måter. Her er en innføring som forklarer mer om denne prosessen.

Steg 1. Eksporterer til CSV-filer fra MySQL-tabeller.

Dette steget gjøres fra MySQL.


(SELECT 'PostNr','Poststed')
UNION 
SELECT * FROM Poststed
INTO OUTFILE 'C:/tmp/Poststed.csv' 
FIELDS ENCLOSED BY '' 
TERMINATED BY ';' 
ESCAPED BY '"' 
LINES TERMINATED BY '\r\n';

(SELECT 'AnsNr','Fornavn', 'Etternavn', 'Adresse', 'PostNr', 'Fødselsdato', 'Kjønn', 'Stilling', 'Årslønn')
UNION 
SELECT * FROM Ansatt
INTO OUTFILE 'C:/tmp/Ansatt.csv' 
FIELDS ENCLOSED BY '' 
TERMINATED BY ';' 
ESCAPED BY '"' 
LINES TERMINATED BY '\r\n';

(SELECT 'KNr','Fornavn', 'Etternavn', 'Adresse', 'PostNr')
UNION 
SELECT * FROM Kunde
INTO OUTFILE 'C:/tmp/Kunde.csv' 
FIELDS ENCLOSED BY '' 
TERMINATED BY ';' 
ESCAPED BY '"' 
LINES TERMINATED BY '\r\n';

(SELECT 'KatNr','Navn')
UNION 
SELECT * FROM Kategori
INTO OUTFILE 'C:/tmp/Kategori.csv' 
FIELDS ENCLOSED BY '' 
TERMINATED BY ';' 
ESCAPED BY '"' 
LINES TERMINATED BY '\r\n';

(SELECT 'VNr', 'Betegnelse', 'Pris', 'KatNr', 'Antall', 'Hylle')
UNION 
SELECT * FROM Vare
INTO OUTFILE 'C:/tmp/Vare.csv' 
FIELDS ENCLOSED BY '' 
TERMINATED BY ';' 
ESCAPED BY '"' 
LINES TERMINATED BY '\r\n';

(SELECT 'VNr', 'Dato', 'GammelPris')
UNION 
SELECT * FROM Prishistorikk
INTO OUTFILE 'C:/tmp/Prishistorikk.csv' 
FIELDS ENCLOSED BY '' 
TERMINATED BY ';' 
ESCAPED BY '"' 
LINES TERMINATED BY '\r\n';

(SELECT 'OrdreNr', 'OrdreDato', 'SendtDato', 'BetaltDato', 'KNr')
UNION 
SELECT * FROM Ordre
INTO OUTFILE 'C:/tmp/Ordre.csv' 
FIELDS ENCLOSED BY '' 
TERMINATED BY ';' 
ESCAPED BY '"' 
LINES TERMINATED BY '\r\n';

(SELECT 'OrdreNr', 'VNr', 'PrisPrEnhet', 'Antall')
UNION 
SELECT * FROM Ordrelinje
INTO OUTFILE 'C:/tmp/Ordrelinje.csv' 
FIELDS ENCLOSED BY '' 
TERMINATED BY ';' 
ESCAPED BY '"' 
LINES TERMINATED BY '\r\n';

Steg 2. Importerer CSV-filer til Neo4j.

Kommandoene herfra og ut kjøres fra Neo4j.

Hver rad blir til en node med et attributt for hver kolonne, og hver tabell blir til en samling av noder som tilhører samme nodetype. Vi oppretter noder og attributter også for koblingstabeller og fremmednøkler i første omgang.

Starter med å flytte CSV-filene fra C:/tmp (se forrige steg) til en web-tjener. Erstatt URL-en www.xyz.no med URL-en til CSV-filene i følgende LOAD-kommandoer:


USING PERIODIC COMMIT
LOAD CSV WITH HEADERS FROM "https://www.xyz.no/Poststed.csv" AS row
FIELDTERMINATOR ';'
CREATE (:Poststed {postNr: row.PostNr, poststed: row.Poststed});

USING PERIODIC COMMIT
LOAD CSV WITH HEADERS FROM "https://www.xyz.no/Ansatt.csv" AS row
FIELDTERMINATOR ';'
CREATE (:Ansatt {ansNr: row.AnsNr, fornavn: row.Fornavn, etternavn: row.Etternavn, adresse: row.Adresse, postNr: row.PostNr, fødselsdato: row.Fødselsdato, kjønn: row.Kjønn, stilling: row.Stilling, årslønn: row.Årslønn});

USING PERIODIC COMMIT
LOAD CSV WITH HEADERS FROM "https://www.xyz.no/Kunde.csv" AS row
FIELDTERMINATOR ';'
CREATE (:Kunde {kNr: row.KNr, fornavn: row.Fornavn, etternavn: row.Etternavn, adresse: row.Adresse, postNr: row.PostNr});

USING PERIODIC COMMIT
LOAD CSV WITH HEADERS FROM "https://www.xyz.no/Kategori.csv" AS row
FIELDTERMINATOR ';'
CREATE (:Kategori {katNr: row.KatNr, navn: row.Navn});

USING PERIODIC COMMIT
LOAD CSV WITH HEADERS FROM "https://www.xyz.no/Vare.csv" AS row
FIELDTERMINATOR ';'
CREATE (:Vare {vNr: row.VNr, betegnelse: row.Betegnelse, pris: row.Pris, katNr: row.KatNr, antall: row.Antall, hylle: row.Hylle});

USING PERIODIC COMMIT
LOAD CSV WITH HEADERS FROM "https://www.xyz.no/Prishistorikk.csv" AS row
FIELDTERMINATOR ';'
CREATE (:Prishistorikk {vNr: row.VNr, dato: row.Dato, gammelPris: row.GammelPris});

USING PERIODIC COMMIT
LOAD CSV WITH HEADERS FROM "https://www.xyz.no/Ordre.csv" AS row
FIELDTERMINATOR ';'
CREATE (:Ordre {ordreNr: row.OrdreNr, ordreDato: row.OrdreDato, sendtDato: row.SendtDato, betaltDato: row.BetaltDato, kNr: row.KNr});

USING PERIODIC COMMIT
LOAD CSV WITH HEADERS FROM "https://www.xyz.no/Ordrelinje.csv" AS row
FIELDTERMINATOR ';'
CREATE (:Ordrelinje {ordreNr: row.OrdreNr, vNr: row.VNr, prisPrEnhet: row.PrisPrEnhet, antall: row.Antall});

Steg 3. Oppretter nodenøkler.


CREATE CONSTRAINT ON (p:Poststed)       ASSERT (p.postNr) IS NODE KEY;
CREATE CONSTRAINT ON (a:Ansatt)         ASSERT (a.ansNr) IS NODE KEY;
CREATE CONSTRAINT ON (k:Kunde)          ASSERT (k.kNr) IS NODE KEY;
CREATE CONSTRAINT ON (k:Kategori)       ASSERT (k.katNr) IS NODE KEY;
CREATE CONSTRAINT ON (v:Vare)           ASSERT (v.vNr) IS NODE KEY;
CREATE CONSTRAINT ON (o:Ordre)          ASSERT (o.ordreNr) IS NODE KEY;

Prishistorikk og Ordrelinje er svake, med sammensatte og (delvis) arvede primærnøkler – som også er fremmednøkler i en relasjonsdatabase. Vi kunne laget sammensatte nodenøkler også for disse, slik:


CREATE CONSTRAINT ON (ph:Prishistorikk) ASSERT (ph.vNr, ph.dato) IS NODE KEY;
CREATE CONSTRAINT ON (ol:Ordrelinje)    ASSERT (ol.ordreNr, ol.vNr) IS NODE KEY;

Men de arvede "fremmednøklene" vil etter hvert bli erstattet av en kant – og det ser ikke ut til at man kan opprette sammensatte nodenøkler der en av "komponentene" er en kant.

Steg 4. Oppretter indekser for "fremmednøkler".

Hvis vi erstatter attributtene som tilsvarer fremmednøklene med kanter (se lenger ned), så vil disse indeksene også bli fjernet og man kan diskutere om dette steget er hensiktsmessig.


CREATE INDEX ON :Ansatt(postNr);
CREATE INDEX ON :Kunde(postNr);
CREATE INDEX ON :Vare(katNr);
CREATE INDEX ON :Prishistorikk(vNr);
CREATE INDEX ON :Ordre(kNr);
CREATE INDEX ON :Ordrelinje(ordreNr);
CREATE INDEX ON :Ordrelinje(vNr);

Steg 5. Oppretter kanter ("forhold") for å erstatte en-til-mange forhold.


MATCH (a:Ansatt),(p:Poststed)
WHERE a.postNr = p.postNr
CREATE (a)-[:BOSTED]->(p);

MATCH (k:Kunde),(p:Poststed)
WHERE k.kNr = p.postNr
CREATE (k)-[:BOSTED]->(p);

MATCH (v:Vare),(k:Kategori)
WHERE v.katNr = k.katNr
CREATE (v)-[:TILHØRER]->(k);

MATCH (p:Prishistorikk),(v:Vare)
WHERE p.vNr = v.vNr
CREATE (p)-[:FOR]->(v);

MATCH (o:Ordre),(k:Kunde)
WHERE o.kNr = k.kNr
CREATE (o)-[:TILHØRER]->(k);

MATCH (ol:Ordrelinje),(o:Ordre)
WHERE ol.ordreNr = o.ordreNr
CREATE (ol)-[:TILHØRER]->(o);

MATCH (ol:Ordrelinje),(v:Vare)
WHERE ol.vNr = v.vNr
CREATE (ol)-[:GJELDER]->(v);

Steg 6. Erstatter "koblingstabeller" av kanter med egenskaper.

Oppretter først mange-til-mange kanter med egenskaper:


MATCH (v:Vare)<-[r1:GJELDER]-(ol:Ordrelinje)-[r2:TILHØRER]->(o:Ordre)
CREATE (o)-[:DETALJER {prisPrEnhet: ol.prisPrEnhet, antall: ol.antall}]->(v);

Fjerner indeksene på fremmednøklene:


DROP INDEX ON :Ordrelinje(ordreNr);
DROP INDEX ON :Ordrelinje(vNr);

Fjerner "koblingstabellen" med tilhørende kanter:


MATCH (ol:Ordrelinje)
DETACH DELETE ol;

Steg 7. Fjerner "fremmednøkler" for (ikke-identifiserende) en-til-mange forhold.

Først indeksene:


DROP INDEX ON :Ansatt(postNr);
DROP INDEX ON :Kunde(postNr);
DROP INDEX ON :Vare(katNr);
DROP INDEX ON :Prishistorikk(vNr);
DROP INDEX ON :Ordre(kNr);

Så selve "fremmednøklene" (dette er altså attributter som er erstattet av kanter):


MATCH (a:Ansatt)
REMOVE a.postNr;

MATCH (k:Kunde)
REMOVE k.postNr;

MATCH (v:Vare)
REMOVE v.katNr;

MATCH (o:Ordre)
REMOVE o.kNr;

I relasjonsdatabasen har Prishistorikk et identifiserende forhold mot Vare med en naturlig (delvis) arvet primærnøkkel – vNr+dato. Hvis vi har opprettet en sammensatt nodenøkkel (se steg 3), kunne vi ikke fjernet egenskapen vNr fordi den ville vært med i den sammensatte nodenøkkelen. Men hvis vi ikke har opprettet en slik nodenøkkel, kan den fjernes slik:


MATCH (p:Prishistorikk)
REMOVE p.vNr;

Hele tabellen Ordrelinje er fjernet i steg 6, men vi kunne alternativt ha fjernet "fremmednøklene" på denne måten:


MATCH (ol:Ordrelinje)
REMOVE ol.ordreNr;

MATCH (ol:Ordrelinje)
REMOVE ol.vNr;

Steg 8. Testspørringer

Vis databaseskjemaet:


CALL db.schema();

Noen eksempelspørringer som sjekker at vi har fått med oss data:


MATCH (m)-[r]->(n)
RETURN *;

MATCH (a:Ansatt)-[:BOSTED]->(p:Poststed)
RETURN a.ansNr, a.etternavn, p.poststed;

MATCH (a:Ansatt)-[r:BOSTED]->(p:Poststed)
RETURN *;

Steg 9. Dump

Neste spørring er ikke testet, men skal eksportere (dumpe) de 100 første nodene (radene) av hver nodetype (tabell) til en fil på mappe c:\tmp.


call apoc.export.cypher.query("match (n)-[r]->(n2) return * limit 100",
 "c:/tmp/hobbyhuset.cypher",
 {format:'plain',separateFiles:true});