Modellierungsproblem: PK des ersten inserts FK des zweiten und umgekehrt

Dabei seit
Okt. 2007
Beiträge
587
#1
Hi,

ich habe folgende (natürlich modifizierbare) Datenbank:

Unbenannt.PNG

Stellt euch folgende transactionen for:

Code:
ID 1 Account 1 TransactionType: Deposit Amount: 50
Soweit kein Problem. Allerdings gibt es nun als TransactionType den Typ Transfer, also von einem auf ein anderes Konto.
Wir haben also eine ausgehende und eine einkommende Transaktion... Das habe ich in den TransactionTypes codiert; Also einmal Transaction(incoming) und einmal Transaction(outgoing). Das Problem ist nun die zwei Transaktionen einzufügen:

Zum Beispiel:

Code:
ID 2 BankAccount 2 TransactionType: Transfer_outgoing Amount 30
ID 3 BankAccount 3 TransactionType: Transfer_incoming Amount 30
So, diese beiden Transaktionen sind ja nun aber miteinander verknüpft. Wenn ich eins von beiden lösche soll ja auch nummer 2 gelöscht werden. Der normale Weg geht da natürlich über FK, aber dann müsste der erste insert ja bereits den zweiten PK beim enfügen kennen der ja erst danach eingefügt wird. Ich stehe also vor einem Problem.

Ich finde auch das eine m<->n Relation nicht wirklich passt weil sie ja nun eigendlich nicht die wirklichkeit widerspiegelt...So wie die Tabelle momentan aufgebaut ist, ist die Abfrage sehr einfach welche transaction zu welchem Konto und welcher Typ es ist inklusive Transfer, nur die verknüofung fehlt.

Jetzt sind die experten gefragt
 
Dabei seit
Juli 2010
Beiträge
4.994
#2
Du kannst in deiner relationalen Datenbank doch einfach eine "Ich-Schlag-Alles-Tot"-Transaktion außenrum machen. Dann sollte das mit der Konsistenz ja kein Problem mehr darstellen.

Alternativ würde ich die transaction Tabelle umbauen, sodass dort 2 Accounts eingetragen werden. Also Sender und Empfänger.
Wenn einfach am Automaten Geld abholt oder eingezahlt wird, setzt man einen der Werte halt auf NULL.
Dann kannst du dir womöglich auch den transaction_type sparen.

Und ganz wichtig: Löschen gibt's bei der Bank nicht!
"+50€ Löschen" heißt: kompensierenden Eintrag erstellen: "-50€ hinzufügen". Evtl. mit Hinweistext, dass da ein Fehler unterlaufen ist und transaction X damit rückgängig gemacht wird.
 
Zuletzt bearbeitet:

Master1991

Lieutenant
Ersteller dieses Themas
Dabei seit
Okt. 2007
Beiträge
587
#3
Was meinst du mit der "Ich-schlag-alles-Tot" Transaktion?


Alternativ würde ich die transaction Tabelle umbauen, sodass dort 2 Accounts eingetragen werden
Genau das meint ich, aber dann wäre der zweite Account ja FK der jeweils anderen Transaktion. Die werden ja aber nacheinander eingefügt sodass ich den FK gar nicht eintragen kann solange ich den PK von Transaktion zwei nicht kenne?


Was das löschen angeht: Guter Hinweis, ich bilde aber keine richtige Bank nach.:)

Bisher war die Abfrage für den jeweiligen Account halt einfach:

Code:
@Query("SELECT transactions.date AS date, transaction_type.name AS type,transactions.amount AS amount, currency.symbol AS symbol, currency.code AS currencyCode, transactions.note AS note, transactions.exchange_rate AS exchangeRate" +
            " FROM transactions JOIN currency ON currency.id = transactions.currency_id" +
            " JOIN transaction_type ON transactions.transaction_type_id=transaction_type.id" +
            " JOIN account ON transactions.account_id = account.id" +
            " WHERE account.name =:accountName" +
            " ORDER BY date DESC")
    public LiveData<List<AccountTransaction>> loadAccountTransactions(String accountName);
 
Dabei seit
Juli 2010
Beiträge
4.994
#4
Irgendwie glaube ich, dass ich dein Problem nicht verstehe.

Die Accounts sind ja bereits vorhanden. Ohne Account kann es ja keine Transaktion geben.

Dann gibt's 3 Fälle für die Transaktion:
1. Geld einzahlen
2. Geld abheben
3. Überweisung

Wenn du in der transaction Tabelle jetzt 2 Account FKs hättest, dann würde das so aussehen:
Geld einzahlen:
Code:
INSERT INTO transaction (account_from, account_to, amount) VALUES (NULL, "irgendeine_account_id", 50)
Geld abheben:
Code:
INSERT INTO transaction (account_from, account_to, amount) VALUES ("irgendeine_account_id", NULL, 50)
Überweisung:
Code:
INSERT INTO transaction (account_from, account_to, amount) VALUES ("account_id_1", "account_id_2", 50)
Wenn du jetzt alle Transaktionen für einen Account haben willst, machst du halt einfach ein
Code:
SELECT ... FROM transactions WHERE account_from = :account_id OR account_to = :account_id ORDER BY date DESC
EDIT: Warum ist der transaction_type eigentlich eine eigene Tabelle? Das sollte doch nur ein Enum oder ähnliches sein mit ein paar fest definierten Typen.
 
Zuletzt bearbeitet:

Master1991

Lieutenant
Ersteller dieses Themas
Dabei seit
Okt. 2007
Beiträge
587
#5
Ich denke, das liegt daran, das ich noch nicht soviel mit SQL gemacht habe und vll einfach auf dem Schlauch stehe. Ich wusste z.B. nicht das es enums gibt (Weiß aber auch nicht ob Android Room dies unterstützt, müsste ich mal nachgucken) Aber die extra Tabelle tut ja auch nicht weh, oder?

Zum Problem:

Wenn ich das mache:
Code:
SELECT ... FROM transactions WHERE account_from = :account_id OR account_to = :account_id ORDER BY date DESC
Dann bekomm ich zwar alle Transaktionen, aber ich möchte ja auch anzeigen auf dem jeweiligen Account ob es eine abgehende oder ankommende Überweisung ist, deshalb hatte ich ja zwei Transaction_types und darauf passt meine Query von oben.

Wenn ich nun den Transfer als eine einzige Transaktion darstelle, dann brauch ich doch zum Abfragen mindestens zwei Querys. Einmal um alle Transaktions von account_from abzufragen (das wären dann ja negative Transfers) und einmal um alle account_to abzufragen um bei einem Transfer entsprechend den type richtig zu stetzen als notiz?
 
Dabei seit
Juli 2010
Beiträge
4.994
#6
Statt Enums kannst du auch einfach einen String nehmen. Bei so einer kleinen Software ist das natürlich egal, aber wenn's dann mal tausende Datenbankabfragen werden, dann tut so ein extra-JOIN schon weh ;)
D.h. ich würde statt der Extra Tabelle einfach einen VARCHAR transaction_type nehmen.

Das mit dem SELECT ist eine Kleinigkeit. Du lässt dir im SELECT jeweils "account_from" und "account_to" mit ausgeben, also
Code:
SELECT account_from, account_to, ... FROM ... (alles wie gehabt)
Und wenn du jetzt wissen willst, ob die Transaktion ein- oder ausgehend ist, dann schaust du einfach, ob die aktuelle account_id = account_from (= ausgehend) oder account_id = account_to (=eingehend) ist.

D.h. dein User Interface macht nicht mehr:
Code:
if(transaction.type.name == "incoming") { ... }
sondern:
Code:
if(currentAccountId == transaction.account_to) { ... }
 

Master1991

Lieutenant
Ersteller dieses Themas
Dabei seit
Okt. 2007
Beiträge
587
#7
Ah, ja das macht sinn. Super das hilft :) Manchmal hat man einfach so denkblockaden.

Was den String angeht. Naja letztendlich ist es ja ein String aber er ist halt in einer extra Tabelle um der realtionalen Datenbank gerecht zu werden. Sprichwort normalenformen. Wenn ich Varchars zur transaction Tabelle hinzufüge habe ich häufig den selben String, ändert sich der muss ich es überall ändern das widerspricht 2NF.

Ich danke dir auf jedenfall, das macht jetzt alles sinn:)
 
Dabei seit
Juli 2010
Beiträge
4.994
#8
Das mit den Normalformen ist so eine Sache für sich.

Ganz grob vereinfacht (und nicht wirklich 100% korrekt) gesagt, kann man das so veranschaulichen:
Tabellen, die eine feste Anzahl von Zeilen haben, die nicht modifiziert werden, sollte man überdenken bzw. streichen und "denormalisieren". In anderen Worten: Die Transaktionstypen sind fest. Wenn du in dieser Tabelle was hinzufügst oder entfernst, dann bist du auch gezwungen die Software anzupassen. Und wenn du dafür eh schon die Software anpassen musst, dann braucht man die Daten auch nicht in einer extra Tabelle speichern.

Dasselbe gilt im Prinzip auch für die Währungen (allerdings nur bedingt), weil es eine feste Anzahl an Währungen gibt und sich diese Anzahl eigentlich nicht ändert. Aber wie gesagt: Hier kann man diskutieren, weil Währungen sich natürlich mal ändern können.
Wenn man die Währungstabelle entfernt, steigert man die Performance. Aber dafür braucht's dann ein Software Update, wenn sich mal was an den Währungen ändern sollte.

Bei den Accounts und Transaktionen ist es eindeutig. Hier braucht es Tabellen, weil hier ständig Datensätze hinzukommen / verändert werden können.

Das mit den doppelten Werten in der Tabelle muss man relativ sehen. Jetzt hast du halt immer wieder die selben 3 IDs in der transaction Tabelle stehen. Macht keinen großen Unterschied.
Als Gegenargument würde ich dann einfach sagen: Dann kannst du für "amount" auch eine Extra Tabelle erstellen. Da kommen ja auch des öfteren dieselben Werte vor. Aber macht ja keiner :D
 

ayngush

Lieutenant
Dabei seit
Okt. 2007
Beiträge
696
#10
Das mit den Normalformen ist so eine Sache für sich.

Ganz grob vereinfacht (und nicht wirklich 100% korrekt) gesagt, kann man das so veranschaulichen:
Tabellen, die eine feste Anzahl von Zeilen haben, die nicht modifiziert werden, sollte man überdenken bzw. streichen und "denormalisieren". In anderen Worten: Die Transaktionstypen sind fest. Wenn du in dieser Tabelle was hinzufügst oder entfernst, dann bist du auch gezwungen die Software anzupassen. Und wenn du dafür eh schon die Software anpassen musst, dann braucht man die Daten auch nicht in einer extra Tabelle speichern.
MVC!
Deine business logic gehört nicht in den View, wer das so baut, baut ganz großen Mist.
 
Dabei seit
Juli 2010
Beiträge
4.994
#11
@ayngus: Was hat die Datenbank und deren Modellierung mit MVC oder einer View zu tun?
Ich hab keine Ahnung was du mit deinem Post sagen wolltest.
 

Ocram1992

Lt. Junior Grade
Dabei seit
Mai 2010
Beiträge
466
#12
[...]D.h. ich würde statt der Extra Tabelle einfach einen VARCHAR transaction_type nehmen.[...]
Tabellen, die eine feste Anzahl von Zeilen haben, die nicht modifiziert werden, sollte man überdenken bzw. streichen und "denormalisieren". In anderen Worten: Die Transaktionstypen sind fest. Wenn du in dieser Tabelle was hinzufügst oder entfernst, dann bist du auch gezwungen die Software anzupassen. Und wenn du dafür eh schon die Software anpassen musst, dann braucht man die Daten auch nicht in einer extra Tabelle speichern.
Mal eine Anmerkung von mir um das Beste aus beiden Welten zu vereinen:

Änder die Tabelle "transaction_type" so, dass es keine Spalte "id" mehr gibt, sondern nur noch "name". Mach "name" zu deinem Primärschlüssel und referenziere "name" in deiner Tabelle "transaction". (Mein ehemaliger Professor nannte das einen sprechenden Schlüssel)

Vorteile
  • Du brauchst keinen JOIN mehr
  • Du kannst ohne JOIN lesen um was für eine Transaction es sich handelt
  • Du behältst die Normalform bei (mit dem Vorteil der Datenintegrität - selbst bei einem 1-Mann-Projekt passieren Rechtschreibfehler, wo Sie nicht passieren sollten -> nervige Fehler die zu suchen sind)

Nachteile
  • Im Code kannst du jetzt kein ENUM mehr verwenden, da ENUMs auf Zahlen basieren. Stattdessen bräuchtest du eine (statische) Klasse mit statischen (konstanten) Feldern, wo die Werte einmal definiert wurden.
  • Mehr Speicherplatzverbrauch durch den Index zwischen "transaction" und "transaction_type" (Den Index könnte man aber auch löschen)
  • Evtl. mehr Speicherplatzverbrauch durch den Index auf dem Primärschlüssel von "transaction_type". (Den Index könnte man aber auch löschen)

Anderer Fall, wenn du die Änderung "Sender" und "Empfänger" als Spalten hinzufügst, dann brauchst du die Tabelle nicht mehr, da du über die Variation von "Sender" und "Empfänger" herausfindest, ob es sich um eine Abhebung, Einzahlung oder Überweisung handelt.

----------------------

Was ganz anderes: Schon bedacht, wie du die Umrechnung von einer Währung in eine andere Währung während einer Überweisung handhabst? Du musst ja den Umrechnungskurs speichern, evtl. Gebühren, etc. Einfach mal so als Gedankenanstoß.
 
Dabei seit
Juli 2010
Beiträge
4.994
#13
Änder die Tabelle "transaction_type" so, dass es keine Spalte "id" mehr gibt, sondern nur noch "name". Mach "name" zu deinem Primärschlüssel und referenziere "name" in deiner Tabelle "transaction". (Mein ehemaliger Professor nannte das einen sprechenden Schlüssel)
Da hast du natürlich Recht, was die Normalform angeht. Aber ich krieg da trotzdem Ausschlag :D Jetzt hat man eine Tabelle, die man gar nicht benutzt, und nur behält, damit man 3NF sagen kann. Na, ich weiß nicht. In z.B. Postgres und MySQL gibt's für sowas eigentlich den "ENUM" Typ.

---

Er kann so oder so im Code weiterhin Enums verwenden. In Java kann ein Enum beliebige Daten transportieren.
Code:
public enum Foo {
  VAL_1("foo", 42),
  VAL_2("bar", 42);

  private String s;
  private int i;

  private Foo(String s, int i) {
    this.s = s;
    this.i = i;
  }

  public String getS() {
    return s;
  }

  public getI() {
    return i;
  }
}
Wenn man Hibernate benutzt für die Persistenz, dann geht's auch noch einfacher:
Code:
public enum Foo {
  VAL_1, VAL_2
}
Und in der Entity kann man dann "@Enumerated(EnumType.STRING)" verwenden. Und schon speichert Hibernate die String Repräsentation des Enums in der Datenbank und nicht den dahinterstehenden Index.
 
Top