SQL bekerja dengan dan mengembalikan data tabular (atau relasi, jika Anda lebih suka menganggapnya seperti itu, tetapi tidak semua tabel SQL adalah relasi). Apa artinya ini adalah bahwa tabel bersarang seperti yang digambarkan dalam pertanyaan bukanlah fitur yang umum. Ada beberapa cara untuk menghasilkan sesuatu dari jenis di Postgresql, misalnya menggunakan array JSON atau komposit, tetapi sangat mungkin untuk hanya mengambil data tabular dan melakukan bersarang dalam aplikasi. Python memiliki itertools.groupby()
, yang cukup sesuai dengan tagihan, mengingat data yang diurutkan.
Error column "incoming.id" must appear in the GROUP BY clause...
mengatakan bahwa non-agregat dalam daftar pilih, memiliki klausa, dll. harus muncul di GROUP BY
klausa atau digunakan secara agregat, agar tidak memiliki nilai tak tentu . Dengan kata lain nilai harus diambil hanya dari beberapa baris dalam grup, karena GROUP BY
memadatkan baris yang dikelompokkan menjadi satu baris , dan siapa pun dapat menebak dari baris mana mereka diambil. Implementasinya mungkin mengizinkan ini, seperti yang dilakukan SQLite dan MySQL dulu, tetapi standar SQL melarangnya. Pengecualian untuk aturan ini adalah ketika ada dependensi fungsional
; GROUP BY
klausa menentukan non-agregat. Pikirkan gabungan antar tabel A dan B dikelompokkan berdasarkan A kunci utama. Tidak peduli baris mana dalam grup, sistem akan memilih nilai untuk A kolom dari, mereka akan sama karena pengelompokan dilakukan berdasarkan kunci utama.
Untuk mengatasi pendekatan 3 poin yang dimaksudkan secara umum, salah satu caranya adalah dengan memilih gabungan dari masuk dan keluar, yang diurutkan berdasarkan cap waktu mereka. Karena tidak ada hierarki warisan setup––karena mungkin tidak ada, saya tidak terbiasa dengan akuntansi––kembali menggunakan Core dan tupel hasil biasa membuat segalanya lebih mudah dalam kasus ini:
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)
Kemudian untuk membentuk struktur bersarang itertools.groupby()
digunakan:
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]
Hasil akhirnya adalah daftar 2-tupel tanggal dan daftar kamus entri dalam urutan menaik. Bukan solusi ORM, tetapi menyelesaikan pekerjaan. Contoh:
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'}])]
Seperti disebutkan, Postgresql dapat menghasilkan hasil yang hampir sama seperti menggunakan array 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()
Kalau sebenarnya Incoming
dan Outgoing
dapat dianggap sebagai anak-anak dari basis yang sama, misalnya Entry
, menggunakan serikat dapat agak otomatis dengan pewarisan tabel beton
:
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
}
Sayangnya menggunakan AbstractConcreteBase
memerlukan panggilan manual ke configure_mappers()
ketika semua kelas yang diperlukan telah ditentukan; dalam hal ini kemungkinan paling awal adalah setelah mendefinisikan User
, karena Account
bergantung padanya melalui hubungan:
from sqlalchemy.orm import configure_mappers
configure_mappers()
Kemudian untuk mengambil semua Incoming
dan Outgoing
dalam satu kueri ORM polimorfik gunakan Entry
:
session.query(Entry).\
filter(Entry.accountID == accountID).\
order_by(Entry.timestamp).\
all()
dan lanjutkan untuk menggunakan itertools.groupby()
seperti di atas pada daftar hasil Incoming
dan Outgoing
.