SQL werkt met en retourneert tabelgegevens (of relaties, als u er liever zo over denkt, maar niet alle SQL-tabellen zijn relaties). Dit houdt in dat een geneste tabel zoals afgebeeld in de vraag niet zo'n veelvoorkomende functie is. Er zijn manieren om iets dergelijks in Postgresql te produceren, bijvoorbeeld met behulp van arrays van JSON of composieten, maar het is heel goed mogelijk om gewoon tabelgegevens op te halen en het nesten in de toepassing uit te voeren. Python heeft itertools.groupby()
, wat redelijk goed past, gezien gesorteerde gegevens.
De fout column "incoming.id" must appear in the GROUP BY clause...
zegt dat niet-aggregaten in de selectielijst, die een clausule hebben, enz. moeten voorkomen in de GROUP BY
clausule of worden gebruikt in een aggregaat, anders hebben ze mogelijk onbepaalde waarden . Met andere woorden, de waarde zou uit slechts een rij in de groep moeten worden gekozen, omdat GROUP BY
condenseert de gegroepeerde rijen tot een enkele rij , en het zou iedereen raden uit welke rij ze werden gekozen. De implementatie zou dit kunnen toestaan, zoals SQLite en MySQL vroeger deed, maar de SQL-standaard verbiedt dit. De uitzondering op de regel is wanneer er een functionele afhankelijkheid
is; de GROUP BY
clausule bepaalt de niet-aggregaten. Denk aan een join tussen tabellen A en B gegroepeerd op A 's primaire sleutel. Het maakt niet uit welke rij in een groep het systeem de waarden zou kiezen voor A 's kolommen uit, ze zouden hetzelfde zijn aangezien de groepering werd gedaan op basis van de primaire sleutel.
Om de 3-punts algemene beoogde aanpak aan te pakken, zou een manier zijn om een combinatie van inkomend en uitgaand te selecteren, geordend op hun tijdstempels. Aangezien er geen erfenishiërarchie is setup - aangezien er misschien niet eens een is, ik ben niet bekend met boekhouding - een terugkeer naar het gebruik van Core en gewone resultaat-tupels maakt het in dit geval gemakkelijker:
incoming = select([literal('incoming').label('type'), Incoming.__table__]).\
where(Incoming.accountID == accountID)
outgoing = select([literal('outgoing').label('type'), Outgoing.__table__]).\
where(Outgoing.accountID == accountID)
all_entries = incoming.union(outgoing)
all_entries = all_entries.order_by(all_entries.c.timestamp)
all_entries = db_session.execute(all_entries)
Om vervolgens de geneste structuur te vormen itertools.groupby()
wordt gebruikt:
date_groups = groupby(all_entries, lambda ent: ent.timestamp.date())
date_groups = [(k, [dict(ent) for ent in g]) for k, g in date_groups]
Het eindresultaat is een lijst met 2-tupels met datums en een lijst met woordenboeken met vermeldingen in oplopende volgorde. Niet helemaal de ORM-oplossing, maar klaart de klus. Een voorbeeld:
In [55]: session.add_all([Incoming(accountID=1, amount=1, description='incoming',
...: timestamp=datetime.utcnow() - timedelta(days=i))
...: for i in range(3)])
...:
In [56]: session.add_all([Outgoing(accountID=1, amount=2, description='outgoing',
...: timestamp=datetime.utcnow() - timedelta(days=i))
...: for i in range(3)])
...:
In [57]: session.commit()
In [58]: incoming = select([literal('incoming').label('type'), Incoming.__table__]).\
...: where(Incoming.accountID == 1)
...:
...: outgoing = select([literal('outgoing').label('type'), Outgoing.__table__]).\
...: where(Outgoing.accountID == 1)
...:
...: all_entries = incoming.union(outgoing)
...: all_entries = all_entries.order_by(all_entries.c.timestamp)
...: all_entries = db_session.execute(all_entries)
In [59]: date_groups = groupby(all_entries, lambda ent: ent.timestamp.date())
...: [(k, [dict(ent) for ent in g]) for k, g in date_groups]
Out[59]:
[(datetime.date(2019, 9, 1),
[{'accountID': 1,
'amount': 1.0,
'description': 'incoming',
'id': 5,
'timestamp': datetime.datetime(2019, 9, 1, 20, 33, 6, 101521),
'type': 'incoming'},
{'accountID': 1,
'amount': 2.0,
'description': 'outgoing',
'id': 4,
'timestamp': datetime.datetime(2019, 9, 1, 20, 33, 29, 420446),
'type': 'outgoing'}]),
(datetime.date(2019, 9, 2),
[{'accountID': 1,
'amount': 1.0,
'description': 'incoming',
'id': 4,
'timestamp': datetime.datetime(2019, 9, 2, 20, 33, 6, 101495),
'type': 'incoming'},
{'accountID': 1,
'amount': 2.0,
'description': 'outgoing',
'id': 3,
'timestamp': datetime.datetime(2019, 9, 2, 20, 33, 29, 420419),
'type': 'outgoing'}]),
(datetime.date(2019, 9, 3),
[{'accountID': 1,
'amount': 1.0,
'description': 'incoming',
'id': 3,
'timestamp': datetime.datetime(2019, 9, 3, 20, 33, 6, 101428),
'type': 'incoming'},
{'accountID': 1,
'amount': 2.0,
'description': 'outgoing',
'id': 2,
'timestamp': datetime.datetime(2019, 9, 3, 20, 33, 29, 420352),
'type': 'outgoing'}])]
Zoals vermeld, kan Postgresql vrijwel hetzelfde resultaat produceren als het gebruik van een array van JSON:
from sqlalchemy.dialects.postgresql import aggregate_order_by
incoming = select([literal('incoming').label('type'), Incoming.__table__]).\
where(Incoming.accountID == accountID)
outgoing = select([literal('outgoing').label('type'), Outgoing.__table__]).\
where(Outgoing.accountID == accountID)
all_entries = incoming.union(outgoing).alias('all_entries')
day = func.date_trunc('day', all_entries.c.timestamp)
stmt = select([day,
func.array_agg(aggregate_order_by(
func.row_to_json(literal_column('all_entries.*')),
all_entries.c.timestamp))]).\
group_by(day).\
order_by(day)
db_session.execute(stmt).fetchall()
Als in feite Incoming
en Outgoing
kan worden gezien als kinderen van een gemeenschappelijke basis, bijvoorbeeld Entry
, kan het gebruik van vakbonden enigszins worden geautomatiseerd met concrete table-overerving
:
from sqlalchemy.ext.declarative import AbstractConcreteBase
class Entry(AbstractConcreteBase, Base):
pass
class Incoming(Entry):
__tablename__ = 'incoming'
id = Column(Integer, primary_key=True)
accountID = Column(Integer, ForeignKey('account.id'))
amount = Column(Float, nullable=False)
description = Column(Text, nullable=False)
timestamp = Column(TIMESTAMP, nullable=False)
account = relationship("Account", back_populates="incomings")
__mapper_args__ = {
'polymorphic_identity': 'incoming',
'concrete': True
}
class Outgoing(Entry):
__tablename__ = 'outgoing'
id = Column(Integer, primary_key=True)
accountID = Column(Integer, ForeignKey('account.id'))
amount = Column(Float, nullable=False)
description = Column(Text, nullable=False)
timestamp = Column(TIMESTAMP, nullable=False)
account = relationship("Account", back_populates="outgoings")
__mapper_args__ = {
'polymorphic_identity': 'outgoing',
'concrete': True
}
Helaas gebruikt u AbstractConcreteBase
vereist een handmatige aanroep naar configure_mappers()
wanneer alle noodzakelijke klassen zijn gedefinieerd; in dit geval is de vroegste mogelijkheid na het definiëren van User
, omdat Account
hangt ervan af via relaties:
from sqlalchemy.orm import configure_mappers
configure_mappers()
Om vervolgens alle Incoming
. op te halen en Outgoing
gebruik in een enkele polymorfe ORM-query Entry
:
session.query(Entry).\
filter(Entry.accountID == accountID).\
order_by(Entry.timestamp).\
all()
en ga verder met het gebruik van itertools.groupby()
zoals hierboven op de resulterende lijst van Incoming
en Outgoing
.