-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy path05_orm_sqlalchemy_features.py
281 lines (239 loc) · 9.87 KB
/
05_orm_sqlalchemy_features.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
# ORM
# Docs:
# https://docs.sqlalchemy.org/en/14/orm/session_basics.html#basics-of-using-a-session
# The ORM typically does not use the Engine directly
# Proxied by a Session-Object in order
# to have the Session-Object decide
# when to talk to the Database and not the User anymore.
# (By default lazy evaluation of CRUD-operations)
# In ORM a Session-Object will handle the job of connecting,
# committing, and releasing connections to this engine.
# Session = Factory for DB-Connections
import sqlalchemy as sa
import sqlalchemy.orm as orm
import utils
engine = sa.create_engine("sqlite:///:memory:")
# Best practice:
# (1) Create Engine & Sessionmaker:
# engine = create_engine(<db_url>)
# Session = sessionmaker(engine)
# (2.a) Create Registry:
# mapper_registry = registry() or create Base-class:
# Base = declarative_base()
# (2.b) Class Definition for Tables:
# @mapper_registry.mapped annotated class or inherited from Base with __tablename__
# (2.c) Create Tables from Registry:
# mapper_registry.metadata.create_all(connection) or
# Base.metadata.create_all(connection)
# (3) Workflow:
# with Session() as session:
# Create + Updates + Deletes: Add objects to session
# session.add(...)
# Queries: Get objects from session
# session.execute(sa.select(User).filter_by(first_name="Spongebob2")).fetchone()
# (1) Create a Sessionmaker as a Factory for Session-Objects
# & put it into global scope as Central control point
# for creating Session-Object throughout your application
# Reason: https://docs.sqlalchemy.org/en/14/orm/session_basics.html#when-do-i-make-a-sessionmaker
Session = orm.sessionmaker(engine)
# Why indirect access to Session with a Sessionmaker?
# A Session is a stateful object for use in a single Thread
# But not made for simultaneous access such as in a web app
# possible to configure later:
# Session.configure(bind=os.getenv("POSTGRES_URL_SQL_ALCHEMY"))
# Workflow:
# Get a Session-Object from the global Sessionmaker-Factory-Object:
with Session() as session:
# Do stuff
# session.add(some_object)
# session.add(some_other_object)
# session.commit()
pass
# Just syntactic sugar for
# rollback in case of exceptions:
# with Session(engine) as session:
# session.begin()
# try:
# session.add(some_object)
# session.add(some_other_object)
# except:
# session.rollback()
# raise
# else:
# session.commit()
# - Translates Tables to Custom-Class-Object & vice versa
# - Writing SQL-Statements as Custom-Class-Object
# - Versioning of Objects for concurrent updates#
# - Custom-Class-Object-Inheritance
# (2.a) Registry for all Mappings: Class / Tables
# Plus Metadata of Tables
mapper_registry = orm.registry()
# You can also inherit from a Base class:
# import sqlalchemy.ext.declarative as ext
# Base = ext.declarative_base()
# class User(Base):
# pass
# (2.b) Declarative Mapping of Class/Table
# for ORM by Class Definitions:
@mapper_registry.mapped
class User:
# Table Value in mapper_registry:
__tablename__ = "user"
# all Columns in Table as static class fields:
id = sa.Column(sa.Integer, primary_key=True)
first_name = sa.Column(sa.String)
last_name = sa.Column(sa.String)
age = sa.Column(sa.Integer)
# foreign key column stored at address-Table
addresses = orm.relationship(
# Table-Class:
"Address",
# Class-Field in the other Table-Class:
back_populates="user")
# define a string representation
# for logging objects
def __repr__(self):
return f"User({self.id}, {self.first_name}, {self.last_name}, {self.age}, addresses: {self.addresses})"
# Joining tables in ORM:
@mapper_registry.mapped
class Address:
__tablename__ = "address"
id = sa.Column(sa.Integer(), primary_key=True)
user_id = sa.Column("user_id", sa.ForeignKey("user.id"))
user = orm.relationship(
# Table-Class:
"User",
# Field in Table-Class:
# back_populates will also change the other table,
# if values are changed here such as changing foreign keys here
back_populates="addresses")
street = sa.Column(sa.String())
def __repr__(self):
return f"Address(id: {self.id}, user_id: {self.user_id}, street: {self.street})"
# (2.c) Create Tables
# We can create Tables from registry's metadata:
with engine.begin() as connection:
# mapper_registry.metadata.drop_all()
mapper_registry.metadata.create_all(connection)
# import csv files: ...
# (3) Workflow
# Create:
utils.print_table(engine, "user")
with Session() as session:
spongebob = User(first_name="Spongebob", last_name="Squarepants", age=99)
patrick = User(first_name="Patrick", last_name="Starfish", age=99, addresses=[Address(street="Starfish Alley 99")])
mr_crabs = User(first_name="Mr.", last_name="Crab", age=99)
# add single item
session.add(spongebob)
# add multiple items
session.add_all([
patrick,
mr_crabs
])
# Send to Database:
session.commit()
utils.print_table(engine, "user")
# Read
with Session() as session:
# !!! "session.execute" returns Named-Tuples but not User-ORM-managed objects !!!
# Returns either None or first element
user_fetchone = session.execute(sa.select(User).filter_by(first_name="Spongebob2")).fetchone()
# first() same as fetchone():
user_first = session.execute(sa.select(User).filter_by(first_name="Spongebob")).first()
try:
# Returns exactly 1 element or raise Excpetion:
user_one = session.execute(sa.select(User).filter_by(first_name="Spongebob2")).one()
except Exception as e:
print(f"session.execute(...).one(): {e}")
user_one = e
##### Difference between: one() vs fetchone()/first() #######
# result.one() will throw an Error, if multiple results or no results are found
# result.fetchone() will return first result of possibly many or None
print(f"user_fetchone: {user_fetchone}")
print(f"user_first: {user_first}")
print(f"user_one: {user_one}")
# "session.query" returns ORM-managed-Objects which we can be updated:
spongebob = session.query(User).filter_by(first_name="Spongebob").first()
# if there is no foreign key, we get an empty list:
print(f"** spongebob.addresses : {spongebob.addresses}")
# Adding objects to 1-to-many-relationship automatically sets all ids, foreign_keys:
# Back_populates will add Address to Address-Table, whenever an address will be added to User
spongebob.addresses.append(Address(street="Spongy Road 123"))
spongebob2 = session.query(User).filter(User.first_name.in_(["Spongebob"])).first()
# no need to add updated object to session once more:
spongebob.first_name = "Spambot"
spongebob2.last_name = "Circlepants"
session.commit()
utils.print_table(engine, "user")
utils.print_table(engine, "address")
# After a commit or rollback, Session invalidates all of its data.
# If you access objects from the session now,
# a new transaction will get implicitly created and a query is executed.
# So there is always a transaction in place by default, even for reads.
# => High Isolation of Transaction
# Querying in ORM:
# All defined class fields act like Column objects producing SQL expresion
expression = User.first_name.in_(["test"])
print(f"Expression: {expression}")
# Difference between namedtuples and ORM-managed objects:
with Session() as session:
# ORM-managed object:
patrick = session.query(User).filter(User.first_name.in_(["Patrick"])).first()
print(f"session.query(User) - Patrick ORM based object: {patrick}")
# accessing one to many field, will lazy evaluate + fetch whole list from DB at time of accessing
print(f"Patrick.addresses lazily loaded (1-to-many): {patrick.addresses}")
# named tuple:
patrick = session.query(User.first_name, User.age).filter(User.first_name.in_(["Patrick"])).first()
print(f"session.query(User.first_name, User.age) - Patrick named tuple: {patrick}")
# Difference between filter(...) and filter_by(...)
# filter accepts *args aka positional arguments
# filter_by accepts **kwargs aka named arguments
# Joins: not really needed
with Session() as session:
# inner join: Users with no addresses will not get fetched here:
rows = session.query(User, Address).join(
Address
# on-clause:
# , User.id == Address.user_id
).all()
for user, address in rows:
user.age = 44
print(f"Join - User: {user}")
print(f"Join - Address: {address}")
# if there is no: session.commit(),
# updates on ORM-managed-objects will not get committed to DB
# session.commit()
# Use relationship for joins for most accurate explicit join on columns:
users = session.query(User).join(
# relationship:
User.addresses
).all()
print(f"join on relationship: {users}")
utils.print_table(engine, "user")
# Delete rows with ORM:
with Session() as session:
# return number of rows deleted:
deleted_count = session.query(User).filter_by(last_name="Crab").delete()
print(f"deleted_count: {deleted_count}")
session.commit()
with Session() as session:
# return number of rows deleted:
patrick = session.query(User).filter_by(last_name="Starfish").first()
print(f"patrick: {patrick}")
# void: returns none
session.delete(patrick)
print(f"deleted_count: {deleted_count}")
session.commit()
# Update rows with ORM:
with Session() as session:
# return number of rows updated:
updated_count = session.query(User).filter_by(last_name="Circlepants").update({"first_name": "Mr"})
print(f"updated_count: {updated_count}")
session.commit()
with Session() as session:
# return number of rows updated:
circle_pants = session.query(User).filter_by(last_name="Circlepants").one()
print(f"circle_pants: {circle_pants}")
circle_pants.age = 12345678990
session.commit()
utils.print_table(engine, "user")