|
| 1 | +# Join table and relation in SQLAlchemy |
| 2 | + |
| 3 | +The purpose of these laboratory classes is to familiarize participants with join table in SQLAlchemy. |
| 4 | + |
| 5 | +The scope of this classes: |
| 6 | + - using join() |
| 7 | + - using outerjoin() |
| 8 | + - |
| 9 | + |
| 10 | +## Introduction |
| 11 | +From the previous classes we know how create query to database in SQLAlchemy based on function [select](https://docs.sqlalchemy.org/en/13/core/metadata.html?highlight=select#sqlalchemy.schema.Table.select) or [query](https://docs.sqlalchemy.org/en/14/orm/query.html) |
| 12 | + |
| 13 | +To work properly in class, we will need the following configuration: |
| 14 | +```python |
| 15 | +from sqlalchemy import create_engine, select, Column, Integer, String, Date, ForeignKey, PrimaryKeyConstraint |
| 16 | +from sqlalchemy.orm import sessionmaker, relationship |
| 17 | +from sqlalchemy.ext.declarative import declarative_base |
| 18 | +from sqlalchemy import create_engine, MetaData, Table |
| 19 | + |
| 20 | +engine = create_engine(db_string) |
| 21 | + |
| 22 | +metadata = MetaData() |
| 23 | + |
| 24 | +dic_table = {} |
| 25 | +for table_name in engine.table_names(): |
| 26 | + dic_table[table_name] = Table(table_name, metadata , autoload=True, autoload_with=engine) |
| 27 | + |
| 28 | +session = (sessionmaker(bind=engine))() |
| 29 | + |
| 30 | +Base = declarative_base() |
| 31 | +``` |
| 32 | + |
| 33 | +The first part of the laboratory will concern the case of working with a database whose structure is don't well known. |
| 34 | + |
| 35 | +All the examples for this laboratory part will be for the country, city, and address tables that are mapped on the classes: |
| 36 | + |
| 37 | +```python |
| 38 | +class Country(Base): |
| 39 | + __tablename__ = 'country' |
| 40 | + country_id = Column(Integer, primary_key=True) |
| 41 | + country = Column(String(50)) |
| 42 | + last_update = Column(Date) |
| 43 | + def __str__(self): |
| 44 | + return 'Country id:{0}\n Country name: {1}\n Country last_update: {2}'.format(self.country_id,self.country,self.last_update) |
| 45 | + |
| 46 | + |
| 47 | +class City(Base): |
| 48 | + __tablename__ = 'city' |
| 49 | + city_id = Column(Integer, primary_key=True) |
| 50 | + city = Column(String(50)) |
| 51 | + country_id = Column(Integer, ForeignKey('country.country_id')) |
| 52 | + last_update = Column(Date) |
| 53 | + |
| 54 | +class Address(Base): |
| 55 | + __tablename__ = 'address' |
| 56 | + address_id = Column(Integer, primary_key=True) |
| 57 | + address = Column(String(50)) |
| 58 | + address2 = Column(String(50)) |
| 59 | + district = Column(String(50)) |
| 60 | + city_id = Column(Integer, ForeignKey('city.city_id')) |
| 61 | + postal_code = Column(String(10)) |
| 62 | + phone = Column(String(50)) |
| 63 | + last_update = Column(Date) |
| 64 | +``` |
| 65 | + |
| 66 | +## Basic join |
| 67 | + |
| 68 | +To make join we can use script: |
| 69 | + |
| 70 | +```python |
| 71 | +from sqlalchemy import select |
| 72 | + |
| 73 | +# select * from category |
| 74 | + |
| 75 | +mapper_stmt = select([dic_table['city']]).select_from(dic_table['city'].join(dic_table['country'], dic_table['city'].c.country_id == dic_table['country'].c.country_id )) |
| 76 | +print('Mapper join: ') |
| 77 | +print(mapper_stmt) |
| 78 | + |
| 79 | +session_stmt = session.query(Country).join(City) |
| 80 | +print('\nSession join: ') |
| 81 | +print(session_stmt) |
| 82 | +``` |
| 83 | + |
| 84 | +```sql |
| 85 | +Mapper join: |
| 86 | +SELECT city.city_id, city.city, city.country_id, city.last_update |
| 87 | +FROM city JOIN country ON city.country_id = country.country_id |
| 88 | + |
| 89 | +Session join: |
| 90 | +SELECT country.country_id AS country_country_id, country.country AS country_country, country.last_update AS country_last_update |
| 91 | +FROM country JOIN city ON country.country_id = city.country_id |
| 92 | +``` |
| 93 | +As you can see, the join function creates queries that connect tables in a natural way (PK - FK relationship). But the query results will only appear for the columns contained in the table specified in the select or query functions. |
| 94 | + |
| 95 | +To download records for selected tables, modify the code as follows: |
| 96 | +```python |
| 97 | +mapper_stmt = select([dic_table['city'],dic_table['country']]).select_from(dic_table['city'].join(dic_table['country'], dic_table['city'].c.country_id == dic_table['country'].c.country_id )) |
| 98 | +print('Mapper join: ') |
| 99 | +print(mapper_stmt) |
| 100 | + |
| 101 | +session_stmt = q =session.query(Country,City) |
| 102 | +print('\nSession join: ') |
| 103 | +print(session_stmt) |
| 104 | +``` |
| 105 | +After execute mapper_stmt query, we get a list of tuples representing the values of joined table rows. Examples: |
| 106 | + |
| 107 | +```python |
| 108 | +#select query: |
| 109 | +[(1, 'A Corua (La Corua)', 87, datetime.datetime(2006, 2, 15, 9, 45, 25), 87, 'Spain', datetime.datetime(2006, 2, 15, 9, 44)), |
| 110 | +(2, 'Abha', 82, datetime.datetime(2006, 2, 15, 9, 45, 25), 82, 'Saudi Arabia', datetime.datetime(2006, 2, 15, 9, 44)), |
| 111 | +(3, 'Abu Dhabi', 101, datetime.datetime(2006, 2, 15, 9, 45, 25), 101, 'United Arab Emirates', datetime.datetime(2006, 2, 15, 9, 44)), |
| 112 | +(4, 'Acua', 60, datetime.datetime(2006, 2, 15, 9, 45, 25), 60, 'Mexico', datetime.datetime(2006, 2, 15, 9, 44)), |
| 113 | +(5, 'Adana', 97, datetime.datetime(2006, 2, 15, 9, 45, 25), 97, 'Turkey', datetime.datetime(2006, 2, 15, 9, 44)), |
| 114 | +(6, 'Addis Abeba', 31, datetime.datetime(2006, 2, 15, 9, 45, 25), 31, 'Ethiopia', datetime.datetime(2006, 2, 15, 9, 44)), |
| 115 | +...] |
| 116 | +``` |
| 117 | + |
| 118 | +When session_stmt is used, the results are a list of tuples which consist of classes representing the relevant objects |
| 119 | + |
| 120 | +```python |
| 121 | +#session query: |
| 122 | +[(<__main__.Country object at 0x000001CE78E50E88>, <__main__.City object at 0x000001CE78E50FC8>), |
| 123 | +(<__main__.Country object at 0x000001CE7A09C148>, <__main__.City object at 0x000001CE78E50FC8>), |
| 124 | +(<__main__.Country object at 0x000001CE7A09C208>, <__main__.City object at 0x000001CE78E50FC8>), |
| 125 | +(<__main__.Country object at 0x000001CE7A09C288>, <__main__.City object at 0x000001CE78E50FC8>), |
| 126 | +(<__main__.Country object at 0x000001CE7A09C308>, <__main__.City object at 0x000001CE78E50FC8>)] |
| 127 | +``` |
| 128 | + |
| 129 | +If we want to create a query for joined anather table we use the following pattern: |
| 130 | + |
| 131 | +```python |
| 132 | +mapper_stmt = select([dic_table['city']]).\ |
| 133 | +select_from(\ |
| 134 | +dic_table['city'].join(\ |
| 135 | +dic_table['country'], dic_table['city'].c.country_id == dic_table['country'].c.country_id\ |
| 136 | +).join(\ |
| 137 | +dic_table['address'], dic_table['city'].c.city_id == dic_table['address'].c.city_id)\ |
| 138 | +) |
| 139 | + |
| 140 | +session_stmt = session.query(Country,City,Address).\ |
| 141 | +join(City, Country.country_id == City.country_id).\ |
| 142 | +join(Address, Address.city_id == City.city_id) |
| 143 | +``` |
| 144 | +Replacing with the join() function, the outerjoin() function can be used. |
| 145 | + |
| 146 | + |
| 147 | +## Join with conditions |
| 148 | + |
| 149 | +To start filtering according to a given criterion: |
| 150 | +- mapper option: |
| 151 | +```python |
| 152 | +mapper_stmt = select([dic_table['category'].columns.category_id,dic_table['category'].columns.name]).where(dic_table['category'].columns.name == 'Games') |
| 153 | + |
| 154 | +``` |
| 155 | +- session option: |
| 156 | +```python |
| 157 | +session_stmt = session.query(Country).outerjoin(City).filter(Country.country_id > 10) |
| 158 | + |
| 159 | +``` |
| 160 | + |
| 161 | +We can use ouer conditions, such as:: |
| 162 | +- or_ |
| 163 | +- and_ |
| 164 | +- in_ |
| 165 | +- order_by |
| 166 | +- limit |
| 167 | +- etc. |
| 168 | + |
| 169 | +## Join a database with relationships |
| 170 | + |
| 171 | +This section presents issues related to the use of relationships described in table mapping classes. For a better understanding of the topic, a simple database will be created containing two tables of users and their posts. |
| 172 | + |
| 173 | +We can create this database by code: |
| 174 | + |
| 175 | +```python |
| 176 | +from sqlalchemy import create_engine, ForeignKey, Column, Integer, String |
| 177 | +from sqlalchemy.ext.declarative import declarative_base |
| 178 | +from sqlalchemy.orm import relationship, sessionmaker |
| 179 | + |
| 180 | +engine = create_engine(database_url`) |
| 181 | +Base = declarative_base() |
| 182 | +Session = sessionmaker(bind = engine) |
| 183 | +session = Session() |
| 184 | + |
| 185 | +class User(Base): |
| 186 | + __tablename__ = 'users' |
| 187 | + |
| 188 | + id = Column(Integer, primary_key = True) |
| 189 | + name = Column(String) |
| 190 | + address = Column(String) |
| 191 | + email = Column(String) |
| 192 | + def __str__(self): |
| 193 | + return 'User id {}\n name: {}\n address: {}\n email: {}\n'.format(self.id,self.name,self.address,self.email) |
| 194 | +``` |
| 195 | + |
| 196 | +Class User is the simple class that described the data of the user in the system. Class Post represents posts written by a user in the system: |
| 197 | + |
| 198 | +```python |
| 199 | +class Post(Base): |
| 200 | + __tablename__ = 'posts' |
| 201 | + |
| 202 | + id = Column(Integer, primary_key = True) |
| 203 | + user_id = Column(Integer, ForeignKey('users.id')) |
| 204 | + post_text = Column(String) |
| 205 | + users = relationship("User", back_populates = "posts") |
| 206 | + def __str__(self): |
| 207 | + return 'Post id {}\n user_id: {}\n Post: {}\n'.format(self.id,self.user_id,self.post_text) |
| 208 | +``` |
| 209 | +This class also creates a relationship between the users and posts tables. |
| 210 | + |
| 211 | +The last step is to define the relation between posts and user and create the database structure: |
| 212 | +```python |
| 213 | +User.posts = relationship("Post", order_by = Post.id, back_populates = "users") |
| 214 | +Base.metadata.create_all(engine) |
| 215 | +``` |
| 216 | + |
| 217 | +```python |
| 218 | +data_set = [ |
| 219 | + User( |
| 220 | + name = "Anna Mała", |
| 221 | + address = "Small Place", |
| 222 | + email = "am@gmail.com", |
| 223 | + posts = [Post(post_text= 'Omnia vincit Amor'), Post(post_text = 'Cogito ergo sum')] |
| 224 | + ), |
| 225 | + User( |
| 226 | + name = "Marta Kwas", |
| 227 | + address = "Acid avenue", |
| 228 | + email = "acidM@gmail.com", |
| 229 | + posts = [Post(post_text= 'You see, in this world there\'s two kinds of people, my friend: Those with loaded guns and those who dig. You dig.'), Post(post_text= 'I\'m gonna make him an offer he can\'t refuse.')] |
| 230 | + ), |
| 231 | + User( |
| 232 | + name = "Zofia Pompa", |
| 233 | + address = "Water street", |
| 234 | + email = "zpws@gmail.com", |
| 235 | + posts = [Post(post_text= 'Not all those who wander are lost.'), Post(post_text= 'The Answer to the ultimate question of Life, The Universe and Everything is…42!')] |
| 236 | + ) |
| 237 | +] |
| 238 | + |
| 239 | +session.add_all(data_set) |
| 240 | +session.commit() |
| 241 | +``` |
| 242 | +After executing the query: |
| 243 | +```python |
| 244 | +result = session.query(User).join(Post).all() |
| 245 | +print(result) |
| 246 | +``` |
| 247 | +We get a list of users: |
| 248 | +``` |
| 249 | +[<__main__.User at 0x1b965cce048>, |
| 250 | + <__main__.User at 0x1b96541fcc8>, |
| 251 | + <__main__.User at 0x1b96540c148>] |
| 252 | +``` |
| 253 | +But in this case, by corresponding relationship mapping, each user has a posts field with a list of his posts. We can print all retrieved data with the following code: |
| 254 | + |
| 255 | +```python |
| 256 | +for user in result: |
| 257 | + print(user) |
| 258 | + for post in user.posts: |
| 259 | + print(post) |
| 260 | +``` |
| 261 | +Expected result: |
| 262 | +``` |
| 263 | +User id 1 |
| 264 | + name: Anna Mała |
| 265 | + address: Small Place |
| 266 | + email: am@gmail.com |
| 267 | + |
| 268 | +Post id 1 |
| 269 | + user_id: 1 |
| 270 | + Post: Omnia vincit Amor |
| 271 | + |
| 272 | +Post id 2 |
| 273 | + user_id: 1 |
| 274 | + Post: Cogito ergo sum |
| 275 | + |
| 276 | +User id 2 |
| 277 | + name: Marta Kwas |
| 278 | + address: Acid avenue |
| 279 | + email: acidM@gmail.com |
| 280 | + |
| 281 | +Post id 3 |
| 282 | + user_id: 2 |
| 283 | + Post: You see, in this world there's two kinds of people, my friend: Those with loaded guns and those who dig. You dig. |
| 284 | + |
| 285 | +Post id 4 |
| 286 | + user_id: 2 |
| 287 | + Post: I'm gonna make him an offer he can't refuse. |
| 288 | + |
| 289 | +User id 3 |
| 290 | + name: Zofia Pompa |
| 291 | + address: Water street |
| 292 | + email: zpws@gmail.com |
| 293 | + |
| 294 | +Post id 5 |
| 295 | + user_id: 3 |
| 296 | + Post: Not all those who wander are lost. |
| 297 | + |
| 298 | +Post id 6 |
| 299 | + user_id: 3 |
| 300 | + Post: The Answer to the ultimate question of Life, The Universe and Everything is…42! |
| 301 | +``` |
| 302 | + |
| 303 | +## Exercise |
| 304 | + |
| 305 | +Use all of these methods to create queries for the test database. Check their execution time using the [profiling and timing code methods](https://jakevdp.github.io/PythonDataScienceHandbook/01.07-timing-and-profiling.html). |
| 306 | + |
| 307 | +For queries: |
| 308 | +1. View a list of the names and surnames of managers living in the same country and working in the same store. |
| 309 | +2. Find a list of all movies of the same length. |
| 310 | +3. Find all clients living in the same city. |
| 311 | + |
| 312 | + |
| 313 | + |
0 commit comments