ID Generator

Code-Stücke können hier veröffentlicht werden.
Antworten
frabron
User
Beiträge: 306
Registriert: Dienstag 31. März 2009, 14:36

Hallo,

für einen Dateikonverter benötigte ich ein Werkzeug, dass mir Datenbank - Ids generieren kann. Diese Ids müssen relativ flexibel sein, da in der Datenbank eine Vielzahl von unterschiedlichen Primärschlüsseln vorkommen können. Der häufigste Fall sind Primärschlüssel vom Typ Varchar, so dass die Datenbank keine eindeutige Id selber vergeben kann. Es gibt aber auch tinyint Primärschlüssel ohne Autoincrement. Ausserdem profitiert die Anwendung (das Programm, das die Datenbank nutzt) von sequentiellen Ids.
Das Werkzeug muss also die Ids generieren können, nachsehen, ob die Id schon in der Tabelle existiert und in einem in einem bestimmten Format zurückgeben. Zuerst hatte ich eine Funktion, die das Generieren der Ids übernahm, allerdings war das Implementieren der Sequenz der Ids nicht so einfach zu lösen. Deshalb habe ich eine Iterator Klasse geschrieben, die diese Problematik löst.
Das übergebene Connection - Objekt ist eine Pyodbc - Connection, die ich um ein Listenattribut zur Speicherung der bereits generierten Ids erweitert habe.

Code: Alles auswählen

class IDGenerator(object):
    """Generate a unique id in the given table for the given column
    with given length. Tries to generete sequential id numbers"""

    def __init__(self, connection, table_name, col_name, length, type=int):
        self.connection = connection
        self.length = length
        self.type = type
        self.max_tries = 20
        self.num_tries = 0
        self.id = None
        self.last_id = None
        self.raw_sql = 'SELECT "{0}" FROM "{1}" WHERE "{0}" = ?'
        self.sql = self.raw_sql.format(col_name, table_name)
                   
    def __iter__(self):
        return self
        
    def generate(self):
        """Generate a random id"""
        return random.randrange(1, pow(10, self.length)-1)
                
    def is_free(self, id):
        """Test if a id is still available in the table's column"""
        if id not in self.connection.generated_ids:    
            cursor = self.connection.cursor()
            row = cursor.execute(self.sql, str(id)).fetchone()
            if row:
                self.max_tries += 1
                return False
        return True
        
    def next(self):
        while self.num_tries <= self.max_tries:
            # if there's no id generate one else increment existing by one
            if not self.id:
                self.id = self.generate()
            else:
                self.id += 1
            # add the generated id to the connection's list of generated ids
            # in order to prevent unecessary checks against the database
            self.connection.generated_ids.append(id)
            
            # if id is free, return it or generate a new one
            if self.is_free(self.id):
                return self.type(self.id)
            else:
                self.id = self.generate()
        else:
            raise StopIteration

und so wird's genutzt:

Code: Alles auswählen

my_generator = IDGenerator(connection, 'Flaechennutzung', 'FNr', 19, str)
my_id = next(my_generator)
Vielleicht findet ja der eine oder andere etwas Nützliches an dem Schnipsel (oder erheiterndes :mrgreen: )

Frank
apollo13
User
Beiträge: 827
Registriert: Samstag 5. Februar 2005, 17:53

Ich frag mal nicht wie die Methode self.raw_sql.format(col_name, table_name) aussieht........
frabron
User
Beiträge: 306
Registriert: Dienstag 31. März 2009, 14:36

Wieso? steht doch da?! Das ist keine Methode, sondern String Formatierung. Das ist nur so umständlich, weil sonst die Zeile so unschön von der Einrückung gewesen wäre ... :)

Code: Alles auswählen

        self.raw_sql = 'SELECT "{0}" FROM "{1}" WHERE "{0}" = ?'
        self.sql = self.raw_sql.format(col_name, table_name)
deets

Das Ding kommt augenscheinlich ohne Locking aus. Und ist damit - je nach DB-Engine und Transaktions-Modell - zum scheitern verurteilt. ZB mit Postgres kann es sein, dass deine ID-Abfrage erst funktioniert, aber dann beim commit dir um die Ohren fliegt.

Dafuer haben Datenbanken dann verschiedene atomare ID-Generatoren, Oracle & Postgres zB Sequenzen, MySQL Auto-IDs. MS SQL Server hat bestimmt was aenhliches.

Das sollte man benutzen. Wenn man zB sqlalchemy benutzt, macht das der Wrapper fuer einen.

Insofern wuerde ich ehrlich gesagt lieber die Finger hiervon lassen, es kann zu subtilen Fehlern kommen.
ms4py
User
Beiträge: 1178
Registriert: Montag 19. Januar 2009, 09:37

  • MS-SQL hat Identity.
  • varchar würde ich nicht als PK verwenden.
  • Das random-Erzeugen ist sehr unschön. Falls du wirklich kein Auto-Inc. verwenden kannst, dann bitte ein "select max(id)+1 ". Ein konkurrierender Zugriff muss natürlich abgefangen werden.
„Lieber von den Richtigen kritisiert als von den Falschen gelobt werden.“
Gerhard Kocher

http://ms4py.org/
frabron
User
Beiträge: 306
Registriert: Dienstag 31. März 2009, 14:36

Cool, danke für die vielen Hinweise noch.

@deets: Stimmt, locking habe ich nicht implementiert, da habe ich auch gar nicht dran gedacht. Entwickelt habe ich die Klasse für eine Sybase Datenbank, die per ODBC angesprochen wird. Zumindest bei mir beim Testen war es so, sobald eine Verbindung zur Datei (ähnlich wie bei Sqlite) aufgebaut war, konnte keine andere Anwendung auf die Datenbank/Datei zugreifen. Sybase hat bestimmt auch einen Generator für Ids, der wäre für meinen Zweck nur nicht brauchbar. Die Anwendung hat Primary Keys vom Typ Char(10), Char(19), char(16), Int(4) und noch ein paar andere. Wie man sieht ist die Länge als auch der Datentyp sehr unterschiedlich. Aber irgendwie musste ich ja einen PK generieren. Benutzereingaben an der Stelle sind wohl nicht gefragt :mrgreen:

@ms4py: Danke auch für deine Hinweise. Mit dem varchar als PK habe ich ja schon oben geschrieben, das ist nicht auf meinem Mist gewachsen. Ich muss es halt nehmen wie es ist, mein Import-Werkzeug muss halt mit den Strukturen so umgehen können, wie sie vorliegen. Das mit select max() muss ich erst testen, ich bin noch nicht so überzeuzgt, ob das auf Char Spalten das gewünsche Ergebnis liefert.

Danke nochmals,

Frank


Nachtrag von grade zu PKs vom Typ Varchar:
Varchars sind natürlich valide als PK, natürlich nur dort, wo es Sinn macht. Z.B. eine Email oder ein Benutzername, der einzigartig sein muss, Steuernummern o.ä. sind da so Einsatzfälle, die mir in den Sinn kommen. Bei der Anwendung oben ist es aber so, dass da so Dinger wie T100 - T200 als PK vergeben werden, und das ist natürlich klares Missdesign. Mir kommen allerdings oft Anwendungen unter die Finger, die mit Char PKs nicht umgehen können, so dass man letztendlich doch numerische PKs einführen muss.
deets

Also wenn ich deine letzten Worte richtig deute, dann sind es schon "quasi-numerische" IDs. Das heisst, du koenntest eine ID-Tabelle mit auto-increment oder was auch immer benutzen, die garantiert atomar ist. Und dann eben mittels string-formatting dein TXXX daraus basteln.

Und Text-Schluessel in Datenbanken sind entweder intrinsisch gegeben (und somit auch sinnvoll), zB URLs oder Namen (in einer heilen Welt ohne Michael Meyer der 32tausenddreidrittelte) - oder einfach nur schlecht.

Ich bin eh der Ansicht, eine "ordentliche" ID-Spalte schadet nie - ein UNIQUE-Constraint auf einer anderen ID regelt das schon. Aber nicht-numerische IDs sind eigentlich immer schlecht.

Insofern - ein weiterer Grund, das Rezept nicht zu benutzen, denn es loest ein Problem, das man nicht haben sollte, wenn man neu startet. Und wenn man schon was hat.. naja, dann ist die Wahrscheinlichkeit gegen Null, dass es den Anforderungen genuegt.
ms4py
User
Beiträge: 1178
Registriert: Montag 19. Januar 2009, 09:37

Zur Info: In der Regel sollten eindeutige Felder wie Username und Email als "UNIQUE" deklariert werden und als PK eine numerische id vergeben werden. AFAIK sind alle gängige Datenbanken für numerische PKs optimiert.

Edit: Oh, hab deets Eintrag verpasst. Aber wir sind uns ja einer Meinung :)
„Lieber von den Richtigen kritisiert als von den Falschen gelobt werden.“
Gerhard Kocher

http://ms4py.org/
frabron
User
Beiträge: 306
Registriert: Dienstag 31. März 2009, 14:36

Ich bin eh der Ansicht, eine "ordentliche" ID-Spalte schadet nie
Zumindest vom Datenbankdesign her kann man da geteilter Meinung sein, von wegen Speichern von doppelten Informationen und so. Allerdings zeigt auch meine Erfahrung, dass wenn man mit Clients auf die Datenbank zugreifen will (und das will man ja meistens :D), dass man mit numerischen PKs deutlich besser fährt. Deshalb stimme ich da auch mit dir und ms4py überein. Auch wenn man das nicht pauschal für alles übernehmen sollte. Bei einfachen M:N Relationen braucht die Mitteltabelle IMO keinen numerischen PK. Naja, wie auch immer ...
denn es loest ein Problem, das man nicht haben sollte
Das Leben ist kein Wunschkonzert ;) ... die Software, um die es sich hier handelt, kommt aus dem Ingenieursbereich und existiert afaik schon seit über 15 Jahren. Da sind mit Sicherheit einige Leichen im Keller, aber das ist nix, worauf ich auch nur ansatzweise Einfluss habe. Deshalb muss ich mit dem versuchen umzugehen, was ich zur Verfügung habe - halt auch mit den ollen DB-Design, was aber von mir nicht veränderbar ist. Aber wegen den blöden Text-PKs habe ich auch schon ordentlich geflucht - hilf aber alles nix :D
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

frabron hat geschrieben:Bei einfachen M:N Relationen braucht die Mitteltabelle IMO keinen numerischen PK. Naja, wie auch immer ...
Da nach Deiner Ansicht die beteiligten Tabellen ja eh numerische PKs haben, werden die ja entsprechend übernommen ;-)
encoding_kapiert = all(verstehen(lesen(info)) for info in (Leonidas Folien, Blog, Folien & Text inkl. Python3, utf-8 everywhere))
assert encoding_kapiert
frabron
User
Beiträge: 306
Registriert: Dienstag 31. März 2009, 14:36

Höh? Vielleicht liegt's ja an der Uhrzeit, so kurz vor Feierabend, aber ich verstehe nicht so genau, was du meinst, Hyperion ...
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

frabron hat geschrieben:Höh? Vielleicht liegt's ja an der Uhrzeit, so kurz vor Feierabend, aber ich verstehe nicht so genau, was du meinst, Hyperion ...

Code: Alles auswählen

Tabelle 1:
A    B
1    foo
2    bar
A = PK

Tabelle 2:
C    D
1    etwas
42  was anderes
C = PK

-> Tabelle N:M von A und B
A    C
1    1
1    42
2    42
A & C = PK
Die Primärschlüssel der Entities bilden doch zusammengesetzt in der Realtionentabelle den Primärschlüssel. Wenn die Ausgangs-PK eben numerisch sind, ist es der PK in der N:M-Relation auch.
encoding_kapiert = all(verstehen(lesen(info)) for info in (Leonidas Folien, Blog, Folien & Text inkl. Python3, utf-8 everywhere))
assert encoding_kapiert
frabron
User
Beiträge: 306
Registriert: Dienstag 31. März 2009, 14:36

Axo, ja klar. Dann ist das aber doch ein Composite PK, der aus den beiden Spalten gebildtet wird. Ich bezog mich mit meiner Aussage auf deets
Ich bin eh der Ansicht, eine "ordentliche" ID-Spalte schadet nie
die bei dir im Beispiel nicht drin ist. Aber deshalb hast du wahrscheinlich auch den :wink: benutzt. Ich brauch Feierabend :D
deets

frabron hat geschrieben:
Ich bin eh der Ansicht, eine "ordentliche" ID-Spalte schadet nie
Zumindest vom Datenbankdesign her kann man da geteilter Meinung sein, von wegen Speichern von doppelten Informationen und so. Allerdings zeigt auch meine Erfahrung, dass wenn man mit Clients auf die Datenbank zugreifen will (und das will man ja meistens :D), dass man mit numerischen PKs deutlich besser fährt. Deshalb stimme ich da auch mit dir und ms4py überein. Auch wenn man das nicht pauschal für alles übernehmen sollte. Bei einfachen M:N Relationen braucht die Mitteltabelle IMO keinen numerischen PK. Naja, wie auch immer ...
"Pauschal fuer alles" ist niemals gut. Was jetzt eine pauschale Aussage fuer alles ist... ;)

Ich wuerde aber schon so weit gehen, erstmal grundsaetzlich in Frage zu stellen, warum man etwas *anderes* macht als numerische PKs. Insofern wuerde ich das also schon jedem, der hier reinschaut mitgeben als Grundregel: keine nicht-numerischen IDs. Punk. Wenn er oder sie erfahren genug ist, davon abzuweichen, dann braucht es keine Daumenregeln mehr.

denn es loest ein Problem, das man nicht haben sollte
Das Leben ist kein Wunschkonzert ;) ... die Software, um die es sich hier handelt, kommt aus dem Ingenieursbereich und existiert afaik schon seit über 15 Jahren. Da sind mit Sicherheit einige Leichen im Keller, aber das ist nix, worauf ich auch nur ansatzweise Einfluss habe. Deshalb muss ich mit dem versuchen umzugehen, was ich zur Verfügung habe - halt auch mit den ollen DB-Design, was aber von mir nicht veränderbar ist. Aber wegen den blöden Text-PKs habe ich auch schon ordentlich geflucht - hilf aber alles nix :D[/quote]

Du hast mich missverstanden: dass du dir das nicht aussuchen kannst, ist mir klar. Es ging mir in der Ausage aber darum, ob dein Rezept hier von anderen genutzt werden sollte. Und da bin ich der Ansicht: nein. Denn wer seine DB selbst designt, der muss solchen Unfug nicht machen. Und wer sie uebernimmt, der muss es sowieso basierend auf dem basteln, was seine DB ihm vorgibt - es bringt also nix, es zu verwenden.
frabron
User
Beiträge: 306
Registriert: Dienstag 31. März 2009, 14:36

Hallo deets,

ich hatte dich wirklich falsch verstanden, das tut mir leid. Ich fand das Konzept der Generierung von, sagen wir mal, sequentiellen Zufallszahlen von variabler Länge mittels Iterator schick, nicht die Notwendigkeit als solches. Das der Snippet nur bedingt praxistauglich für 99% der Fälle ist, war mir klar, ich hätte es wohl deutlicher dazu schreiben können. Das kommt in der Einleitung nicht deutlich genug heraus.
Die im Snippet verwendeten Techniken hätten mir, hätte ich so etwas noch vor einem Jahr gefunden, geholfen einige Python Konzepte zu verstehen. Das Erzeugen eines Iterators mittels __iter__, next und StopIteration ist, sagen wir mal, nur relativ offensichtlich - für einige mehr, für andere weniger. random.randrange braucht man auch nicht immer und das Erzeugen einer Zahl mit variabler Länge mittels pow() war für mich zumindest auch nicht sofort offensichtlich. Ich zumindest lerne immer gerne vom Code anderer, auch wenn der Code nur bedingt mit aktuellen Problemen meinerseits zu tun hat.
Ich finde den Generator (für mein Verständnis) recht pythonisch in der Verwendung und hatte ihn nur gepostet, um zu zeigen, was machbar ist. Spätestens jetzt weiss ja jeder Bescheid, was Sache ist :)

Frank
Antworten