Seite 1 von 1

Über verschiedene ORMs...

Verfasst: Sonntag 4. Mai 2008, 10:37
von sma
Findet ihr SQLalchemy einfach zu benutzen? Nach einem Klönschnack über verschiedene Wege, ein einfaches Datenmodell zu speichern, hatte ich angefangen, mir dafür etwas zu bauen, nur um dann einzusehen, dass ich wohl das Rad neu erfinde. Also habe ich mir Storm, SQLalchemy uns SQLobject angeschaut und irgendwie ist das alles Schiet.

Mein Datenmodell möge (von Django et.al. inspiriert) so aussehen:

Code: Alles auswählen

class Game(Base):
    turn = Int()
    players = List('Player')
    regions = List('Region')
    
class Player(Base):
    name = Str()
    game = Ref('Game.players')
    allies = List('Player')
    owned_regions = List('Region')
    explored_regions = List('Region')
    
class Region(Base):
    name = Str()
    game = Ref('Game.regions')
    owner = Ref('Player.owned_regions')
Prosa: Zu einem Spiel gibt es Spieler und Regionen. Spieler können miteinander verbündet sein, Regionen besitzen und erforscht haben. Die Rückrichtung für `allies` und `explored_regions` brauche ich nicht.

Damit kann ich jetzt sowas machen:

Code: Alles auswählen

game = Game(turn=1)
game.players.extend((Player('sma'), Player('fto')))
game.regions = (Region(name) for name in 'Anaheim', 'Boslen', 'Ceshire')
game.regions.get(name='Anaheim').owner = game.players.get(name='sma')
print list(game.players.find(lambda p: len(p.allies) > 2))
In SQLalchemy muss ich erstmal Tabellen definieren, dann Klassen, dann die Abbildung dazwischen. Ich muss dabei manuell ID-Spalten anlegen und sehe sie auch in meinem Objektmodell. Oh, und ich muss auch noch das SQL zum Erzeugen der Tabellen selbst schreiben.

Code: Alles auswählen

metadata = sa.MetaData()
games_table = sa.Table('games', metadata,
    sa.Column('id', sa.Integer, primary_key=True),
    sa.Column('turn', sa.Integer, required=True),
)
players_table = sa.Table('players', metadata,
    sa.Column('name', sa.String(30), primary_key=True),
    sa.Column('game_id', sa.Integer, ForeignKey('games.id'), required=True),
)
allies_table = sa.Table('allies', metadata,
    sa.Column('player1_name', sa.String(30), ForeignKey('players.name'))
    sa.Column('player2_name', sa.String(30), ForeignKey('players.name'))
)
regions_table = sa.Table('regions', metadata,
    sa.Column('name', sa.String(30), primary_key=True),
    sa.Column('owner_name', sa.String(30), ForeignKey('players.name')),
    sa.Column('game_id', sa.Integer, ForeignKey('games.id')),
)
explorations_table = sa.Table('explorations', metadata,
    sa.Column('explorer_name', sa.String(30), ForeignKey('players.name')),
    sa.Column('region_name', sa.String(30), ForeignKey('regions.name')),
)
engine = sa.create_engine("sqlite:...")
metadata.create_all(engine)

class Game(object):
    def __init__(self, turn):
        self.turn = turn

class Player(object):
    def __init__(self, name, game):
        self.name, self.game = name, game
    
class Region(object):
    def __init__(self, name, game):
        self.name, self.game = name, game

sa.mapper(Game, games_table, properties={
    'players': sa.relation(Player, backref='game'),
    'regions': sa.relation(Region, backref='game'),
})
sa.mapper(Player, players_table, properties={
    'owned_regions': sa.relation(Region, backref='owner'),
    'explored_regions': sa.relation(Region, secondary=explorations_table),
    'allies': sa.relation(Player, secondary=allies_table),
})
sa.mapper(Region, regions_table)
Ob das so richtig ist, habe ich nicht ausprobiert. Aber ich will mir da eigentlich gar nicht so viele Gedanken machen müssen. Das hantieren mit `session` und die etwas ungelenke Abfragesprachen machen den Rest auch nicht angenehm.

Wenn ich etwa alle Spieler mit mehr als zwei Verbünderten finden will, reicht dann sowas? Oder wie geht es sonst?

Code: Alles auswählen

session.query(Player).filter(len(Player.allies) > 2)
Bei Storm - welches leider total unterdokumentiert ist - kann man sich auf die Objekte konzentrieren. Außerdem gefällt mir das API besser:

Code: Alles auswählen

class Game(object):
    __storm_table__ = 'game'
    id = Int(primary=True)
    turn = Int()
    
class Player(object):
    __storm_table__ = 'player'
    name = Unicode(primary=True)
    game_id = Int()
    game = Reference(game_id, Game.id)
    
class Alliance(object):
    __storm_table__ = 'alliance'
    __storm_primary__ = 'player1_name', 'player2_name'
    player1_name = Unicode()
    player2_name = Unicode()

Game.players = ReferenceSet(Game.id, Player.game_id)
Player.allies = ReferenceSet(
    Player.name, Alliance.player1.name, Alliance.player2_name, Player.name
)

for p in store.find(Player, len(Player.allies) > 2): print p.name
Auch hier muss man die M:N-Tabelle explizit anlegen und auch hier tauchen Datenbank-spezifische IDs im Datenmodell auf. Ich hätte kein Problem, wenn ich von einer Klasse `Base` erben könnte, um ein paar Konventionen zu bekommen, etwa das die Tabelle genauso heißt wie die Klasse oder das `id` der Standard für einen Primärschlüssel ist. Das nachträgliche Definieren der anderen Seite der Relationen ist auch grenzwertig. Gut finde ich, dass Primärschüssel aus mehr als einer Spalte bestehen können (geht bei SQLalchemy garantiert auch).

Bei SQLObject habe ich eine `createTable`-Methode, was ich sehr schön finde. Das Datenmodell sähe wohl so aus:

Code: Alles auswählen

sqlhub.connection = connectionForURI("sqlite:...")

class Game(SQLObject):
    turn = IntCol()

class Player(SQLObject):
    name = StringCol()
    game = ForeignKey('Game')
    allies = RelatedJoin('Player')

Game.sqlmeta.addJoin(MultipleJoin('Player'), joinMethodName='players')

Game.createTable()
Player.createTable()
Mir gefällt, dass sich das Rahmenwerk um die Verbindungstabelle kümmert. Das ich die Gegenrelation nachträglich zuweisen muss, ist allerdings dumm. Auch hat sich SQLObject offenbar dafür entschieden, Java-Camel-Case-Namen zu benutzen und nicht "_". Irgendwie gibt es da bei Python keine klaren Namensvorgaben.

Übrigens, Storm kann offenbar SQLObject emulieren. Da bekommt man dann zwei ORMs für den Preis von einem. Wenn das mal kein gutes Angebot ist ;)

Bei Zodb muss ich gar nichts im Vorfeld definieren. Dafür habe ich Schwierigkeiten, ein gutes Tutorial zu finden. Möglicherweise geht es so:

Code: Alles auswählen

class Game(Persistent):
    def __init__(self, turn):
        self.turn = turn
        self.players = []
        
    def add_player(self, player):
        self.players.append(player)
        self._p_changed = True
        player.game = self
        
    def remove_player(self, player):
        self.players.remove(player)
        self._p_changed = True
        player.game = None
    
    def find_player(self, name):
        for player in self.players:
            if player.name == name: return player

class Player(Persistent):
    def __init__(self, name):
        self.name = name
        self.allies = set()
        
    def add_ally(self, player):
        self.allies.add(player); self._p_changed = True
        player.allies.add(self); player._p_changed = True
        
    def remove_ally(self, player):
        self.allies.remove(player); self._p_changed = True
        player.allies.remove(self); player._p_changed = True

game = Game(1)
game.add_player(Player('sma'))
game.add_player(Player('fto'))
game.find_player('sma').add_ally(game.find_player('fto'))

db = DB(FileStorage.FileStorage('/tmp/...'))
conn = db.open()
conn.root()['game'] = game
transaction.commit()
Hier gibt es keine Relationen, nur Listen und wenn ich diese ändere, darf ich dem System auch noch selbt sagen, dass sich das zugehörige Objekt verändert hat. Ich bin nicht begeistert. Möglicherweise kann Container einsetzen, aus dem, was ich über Grok gelesen habe, schließe ich das jedenfalls. Man könnte sich hier natürlich mit etwas Metamagie selbst helfen und es ähnlich machen, wie ich's bei meinem ersten Beispiel. Oder kommt man sich da mit Zodbs eigener Magie in die Quere?

Es gibt wohl noch "Durus" als Alternative, doch auch dieses Stück Software ist grenzwertig undokumentiert und fühlt sich ungeliebt an. Auch ist deren Transaktionskonzept recht grob (bei Fehlern muss man selbst nochmal von vorn beginnen - erinnert an die GAE).

Ich bilde mir ja ein, dass ich meinen Ansatz zu einer OODB ausbauen könnte (daher hatte ich mir auch Durus angeschaut), tue mich allerdings mit der richtigen Semantik für Transaktionen schwer. Daher wollte ich eigentlich etwas Fertiges haben.

Der Konflikt liegt ja zwischen dem ganzen Objektmodell im Speicher halten (was schnelle Zugriffe erlaubt) und jedes Mal den aktuellen Stand von der Datenbank holen (was aktuelle Daten bedeutet und Fehler oder zumindest Transaktionskonflikte verhindern hilft).

Wer sagt mir etwa, dass ein Spieler, den ich zum Verbündeten machen will, nicht in genau diesem Moment gelöscht wird? Wie stelle ich sicher, dass das (als Beispiel genommen, auch wenn's sicherlich unrealistisch ist) nicht passiert. Muss ich jeweils explizit locken? Unterstützen die erwähnten ORMs das (in den Tutorials jedenfalls nicht)? Sind die ORMs überhaupt threadsafe (Storm ist es nur eingeschränkt)?

Stefan

Verfasst: Sonntag 4. Mai 2008, 11:28
von Leonidas
Du weißt schon, dass es für SQLAlchemy Elixir gibt (was aber in der Tat nicht so besonders toll dokumentiert ist) und inzwischen auch ab 0.4.4 einen kleinen Deklarativen Teil, bei dem du die Tabellen mit den Klassen hübscher verbinden kannst? Der Rest bleibt dann natürlich gleich.

Verfasst: Montag 5. Mai 2008, 13:14
von Y0Gi
Am Rande: Wenn dir der Tabellen-Kram nicht gefällt, solltest du vielleicht auf ein RDBMS sondern eine Objektdatenbank oder ähnliches setzen.

Re: Über verschiedene ORMs...

Verfasst: Montag 5. Mai 2008, 13:24
von Y0Gi
sma hat geschrieben:In SQLalchemy muss ich erstmal Tabellen definieren, dann Klassen, dann die Abbildung dazwischen. Ich muss dabei manuell ID-Spalten anlegen und sehe sie auch in meinem Objektmodell.
Mit dem von Leonidas genannten deklarativen Layer werden die Tabellendefinition und das Mapping implizit über Klassenattribute erzeugt, die zwei Schritte fallen also weg. Einzig benötigte Zwischentabellen muss man explizit definieren.
Wenn dir das zu viel Arbeit und nicht abstrakt genug ist, kannst du mit Elixir einiges an Magie ins Haus holen, was von der Abstraktion etwa der in SQLObject entspricht. Ich persönlich habe den umgekehrten Weg zum deutlicheren, grundlegenden SA (dennoch je nach Projekt/Optimierungsanforderungen mit deklarativem Layer) gegangen, auch wenn ich vorher ebenfalls lange SA für zu übergenau und umständlich hielt.

sma hat geschrieben:Oh, und ich muss auch noch das SQL zum Erzeugen der Tabellen selbst schreiben.
Was? Wir sind doch nicht bei Rails :) Mit `.create_table()` und `.create_all()` (bin mir bei den Namen unsicher, muss das ja selten schreiben) werden Tabellen automatisch mit dem DBMS-spezifischen SQL angelegt.

sma hat geschrieben:Wenn ich etwa alle Spieler mit mehr als zwei Verbünderten finden will, reicht dann sowas? Oder wie geht es sonst?

Code: Alles auswählen

session.query(Player).filter(len(Player.allies) > 2)
Vorweg: Entitäten kann man das `query`-Attribut geben. Beim normalen ORM ist das automatisch so, beim deklarativen Layer muss man das selbst dranhängen), so dass aus `session.query(Player)` das kürzere `Player.query` wird.

Das Query könnte so funktionieren, ja. Am Ende fehlt (ich bin mir nicht 100% sicher) wohl noch ein `.all()`. Falls SA das so nicht erlaubt, steht dann wohl eine Funktion bereit, die man anwenden kann und die das letztlich in SQL umsetzt. Im Notfall muss man auf Subqueries ausweichen.

Verfasst: Samstag 10. Mai 2008, 09:44
von sma
@Leonidas: Den deklarativen Teil von SA 0.4.4 hatte ich mir angeschaut, sah aber einen wesentlichen Unterschied. Elixir ist mir bekannt, allerdings empfinde ich es als eine Schwäche (und nicht als Stärke) von SA, dass so etwas nötig ist. Auch bei Elixir muss ich die N:M-Tabellen explizit als Entity definieren, ich komme also bei der Einfachheit von Storm an. Dann kann ich dies auch gleich benutzen. SQLObjects wäre noch einen Tick einfacher, allerdings missfällt mir da die Menge der Klassenmethoden, die meinen Namensraum einschränken.

Und Elixir hilft mir nicht bei meinen Concurrency-Problemen.

@Y0Gi: Ich hatte die OODBs Durus und Zodb ja schon erwähnt. Mir scheint jedoch, dass Durus so gut wie nirgendwo eingesetzt wird. Zodb ist ja wohl als Teil von Zope in Gebrauch. Durus erfordert regelmäßige Kompaktifizierungen, während denen das System stillsteht. Nicht schön.

Auch ist mir das Transaktionsverhalten nicht 100% klar. Aber damit habe ich wohl eher ein persönliches Verständnisproblem und/oder einen zu hohen Anspruch an Perfektion. Ich glaube, die wenigsten Webanwendungen sind eigentlich transaktionssicher.

Wenn ich mir mit Exilir die Einfachheit von SO für SA holen kann, warum sollte ich nicht direkt SO benutzen? Will fragen: Warum wird eigentlich immer nur SA empfohlen und nie SO? Was ist an SO schlecht? Es hat (im Gegensatz zu Storm, was aus dem Bauch mein Favorit wäre) sogar das "for update", was ich in dem anderen Posting von mir gesucht hatte.

Stefan