SQL arbejder med og returnerer tabeldata (eller relationer, hvis du foretrækker at tænke på det på den måde, men ikke alle SQL-tabeller er relationer). Hvad dette indebærer er, at en indlejret tabel som afbildet i spørgsmålet ikke er så almindelig en funktion. Der er måder at producere noget af den slags i Postgresql, for eksempel ved at bruge arrays af JSON eller kompositter, men det er fuldt ud muligt blot at hente tabeldata og udføre nesting i applikationen. Python har itertools.groupby()
, hvilket passer ganske godt, givet sorterede data.
Fejlen column "incoming.id" must appear in the GROUP BY clause...
siger, at ikke-aggregater i udvalgslisten, havende klausuler osv. skal vises i GROUP BY
klausul eller bruges i en aggregering, så de ikke muligvis har ubestemte værdier . Med andre ord skal værdien kun vælges fra en række i gruppen, fordi GROUP BY
kondenserer de grupperede rækker til en enkelt række , og det ville være enhvers gætte, hvilken række de blev valgt fra. Implementeringen kan tillade dette, ligesom SQLite gør og MySQL plejede at gøre, men SQL-standarden forbyder det. Undtagelsen fra reglen er, når der er en funktionel afhængighed
; GROUP BY
klausulen bestemmer ikke-aggregaterne. Tænk på en joinforbindelse mellem tabellerne A og B grupperet efter A 's primære nøgle. Uanset hvilken række i en gruppe, vil systemet vælge værdierne for A 's kolonner fra, ville de være de samme, da grupperingen blev udført baseret på den primære nøgle.
For at adressere den generelle tilsigtede 3-punkts tilgang ville en måde være at vælge en forening af indgående og udgående, sorteret efter deres tidsstempler. Da der ikke er noget arvehierarki opsætning – da der måske ikke engang er en, jeg er ikke bekendt med regnskab – – en tilbagevenden til at bruge Core og almindelige resultattupler gør tingene lettere i dette tilfælde:
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)
Derefter for at danne den indlejrede struktur itertools.groupby()
bruges:
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]
Slutresultatet er en liste over 2-tupler af dato og en liste over ordbøger over poster i stigende rækkefølge. Ikke helt ORM-løsningen, men får arbejdet gjort. Et eksempel:
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'}])]
Som nævnt kan Postgresql producere stort set det samme resultat, som ved at bruge et array af 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()
Hvis faktisk Incoming
og Outgoing
kan opfattes som børn af en fælles base, for eksempel Entry
, kan brug af fagforeninger være noget automatiseret med konkret tabelarv
:
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
}
Bruger desværre AbstractConcreteBase
kræver et manuelt opkald til configure_mappers()
når alle nødvendige klasser er blevet defineret; i dette tilfælde er den tidligste mulighed efter at have defineret User
, fordi Account
afhænger af det gennem relationer:
from sqlalchemy.orm import configure_mappers
configure_mappers()
Derefter for at hente alle Incoming
og Outgoing
i en enkelt polymorf ORM-forespørgsel skal du bruge Entry
:
session.query(Entry).\
filter(Entry.accountID == accountID).\
order_by(Entry.timestamp).\
all()
og fortsæt med at bruge itertools.groupby()
som ovenfor på den resulterende liste over Incoming
og Outgoing
.