Über verschiedene ORMs...
Verfasst: Sonntag 4. Mai 2008, 10:37
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:
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:
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.
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?
Bei Storm - welches leider total unterdokumentiert ist - kann man sich auf die Objekte konzentrieren. Außerdem gefällt mir das API besser:
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:
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:
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
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')
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))
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)
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)
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
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()
Ü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()
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