SQL arbetar med och returnerar tabelldata (eller relationer, om du föredrar att tänka på det så, men alla SQL-tabeller är inte relationer). Vad detta antyder är att en kapslad tabell som avbildas i frågan inte är så vanlig. Det finns sätt att producera något liknande i Postgresql, till exempel med arrayer av JSON eller kompositer, men det är fullt möjligt att bara hämta tabelldata och utföra kapslingen i applikationen. Python har itertools.groupby()
, vilket passar ganska bra, givet sorterade data.
Felet column "incoming.id" must appear in the GROUP BY clause...
säger att icke-aggregat i urvalslistan, med klausul etc. måste visas i GROUP BY
sats eller användas i ett aggregat, så att de inte har möjligen obestämda värden . Med andra ord skulle värdet behöva väljas från bara någon rad i gruppen, eftersom GROUP BY
kondenserar de grupperade raderna till en enda rad , och det skulle vara någons gissning vilken rad de valdes från. Implementeringen kanske tillåter detta, som SQLite gör och MySQL brukade göra, men SQL-standarden förbjuder sådant. Undantaget från regeln är när det finns ett funktionellt beroende
; GROUP BY
klausulen bestämmer icke-aggregaten. Tänk på en koppling mellan tabellerna A och B grupperad efter A s primärnyckel. Oavsett vilken rad i en grupp skulle systemet välja värdena för A s kolumner från, skulle de vara desamma eftersom grupperingen gjordes baserat på primärnyckeln.
För att ta itu med det allmänna tillvägagångssättet med tre punkter, skulle ett sätt vara att välja en förening av inkommande och utgående, sorterade efter deras tidsstämplar. Eftersom det inte finns någon arvshierarki inställningar – eftersom det kanske inte ens finns en, jag är inte bekant med redovisning – en återgång till att använda Core och vanliga resultattupler gör det enklare i det här fallet:
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)
Sedan för att bilda den kapslade strukturen itertools.groupby()
används:
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 är en lista med 2-tuplar av datum och lista över ordböcker med poster i stigande ordning. Inte riktigt ORM-lösningen, men får jobbet gjort. Ett exempel:
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ämnts kan Postgresql producera ungefär samma resultat som när man använder en array av 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()
Om faktiskt Incoming
och Outgoing
kan ses som barn av en gemensam bas, till exempel Entry
, att använda fackföreningar kan automatiseras något med arv av konkreta tabeller
:
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
}
Använder tyvärr AbstractConcreteBase
kräver ett manuellt anrop till configure_mappers()
när alla nödvändiga klasser har definierats; i detta fall är den tidigaste möjligheten efter att ha definierat User
, eftersom Account
beror på det genom relationer:
from sqlalchemy.orm import configure_mappers
configure_mappers()
Sedan för att hämta alla Incoming
och Outgoing
i en enda polymorf ORM-fråga använd Entry
:
session.query(Entry).\
filter(Entry.accountID == accountID).\
order_by(Entry.timestamp).\
all()
och fortsätt att använda itertools.groupby()
som ovan på den resulterande listan över Incoming
och Outgoing
.