├── .github └── workflows │ └── build_and_deploy.yaml ├── .gitignore ├── .npmignore ├── CONTRIBUTIONS.md ├── README.md ├── package-lock.json ├── package.json ├── src ├── .vuepress │ ├── components │ │ ├── Foo │ │ │ └── Bar.vue │ │ ├── OtherComponent.vue │ │ └── demo-component.vue │ ├── config.js │ ├── enhanceApp.js │ └── styles │ │ ├── index.styl │ │ └── palette.styl ├── en │ ├── index.md │ └── tutorial │ │ ├── 1. Tutorial Overview.md │ │ ├── 2. Setting Up a Connection.md │ │ ├── 3. Executing Transactions and Queries.md │ │ ├── 4. Working with Database Metadata.md │ │ ├── 5.1. Querying Rows Using Core and ORM.md │ │ ├── 5.2. Inserting Rows Using Core.md │ │ ├── 5.3. Modifying and Deleting Rows Using Core.md │ │ ├── 6. Manipulating Data Using ORM.md │ │ ├── 7. Working with Related Objects Using ORM.md │ │ ├── README.md │ │ └── config.js ├── index.md └── tutorial │ ├── 1. 튜토리얼 개요.md │ ├── 2. 연결 설정하기.md │ ├── 3. 트랜잭션과 쿼리 실행하기.md │ ├── 4. 데이터베이스 메타데이터로 작업하기.md │ ├── 5.1. Core와 ORM 방식으로 행 조회하기.md │ ├── 5.2. Core 방식으로 행 삽입하기.md │ ├── 5.3. Core 방식으로 행 수정 및 삭제하기.md │ ├── 6. ORM 방식으로 데이터 조작하기.md │ ├── 7. ORM 방식으로 관련 개체 작업하기.md │ └── README.md └── yarn.lock /.github/workflows/build_and_deploy.yaml: -------------------------------------------------------------------------------- 1 | # ref: https://milooy.wordpress.com/2020/07/28/github-actions%EB%A1%9C-vuepress-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0/ 2 | 3 | name: Build and Deploy 4 | on: 5 | push: 6 | branches: 7 | - main 8 | jobs: 9 | build-and-deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@main 14 | 15 | - name: Vuepress deploy 16 | uses: jenkey2011/vuepress-deploy@master 17 | env: 18 | ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }} 19 | BUILD_SCRIPT: yarn && yarn build 20 | TARGET_BRANCH: gh-pages 21 | BUILD_DIR: src/.vuepress/dist 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/macos,windows,linux,webstorm+all,visualstudiocode,node 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=macos,windows,linux,webstorm+all,visualstudiocode,node 3 | 4 | ### Linux ### 5 | *~ 6 | 7 | # temporary files which can be created if a process still has a handle open of a deleted file 8 | .fuse_hidden* 9 | 10 | # KDE directory preferences 11 | .directory 12 | 13 | # Linux trash folder which might appear on any partition or disk 14 | .Trash-* 15 | 16 | # .nfs files are created when an open file is removed but is still being accessed 17 | .nfs* 18 | 19 | ### macOS ### 20 | # General 21 | .DS_Store 22 | .AppleDouble 23 | .LSOverride 24 | 25 | # Icon must end with two \r 26 | Icon 27 | 28 | 29 | # Thumbnails 30 | ._* 31 | 32 | # Files that might appear in the root of a volume 33 | .DocumentRevisions-V100 34 | .fseventsd 35 | .Spotlight-V100 36 | .TemporaryItems 37 | .Trashes 38 | .VolumeIcon.icns 39 | .com.apple.timemachine.donotpresent 40 | 41 | # Directories potentially created on remote AFP share 42 | .AppleDB 43 | .AppleDesktop 44 | Network Trash Folder 45 | Temporary Items 46 | .apdisk 47 | 48 | ### macOS Patch ### 49 | # iCloud generated files 50 | *.icloud 51 | 52 | ### Node ### 53 | # Logs 54 | logs 55 | *.log 56 | npm-debug.log* 57 | yarn-debug.log* 58 | yarn-error.log* 59 | lerna-debug.log* 60 | .pnpm-debug.log* 61 | 62 | # Diagnostic reports (https://nodejs.org/api/report.html) 63 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 64 | 65 | # Runtime data 66 | pids 67 | *.pid 68 | *.seed 69 | *.pid.lock 70 | 71 | # Directory for instrumented libs generated by jscoverage/JSCover 72 | lib-cov 73 | 74 | # Coverage directory used by tools like istanbul 75 | coverage 76 | *.lcov 77 | 78 | # nyc test coverage 79 | .nyc_output 80 | 81 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 82 | .grunt 83 | 84 | # Bower dependency directory (https://bower.io/) 85 | bower_components 86 | 87 | # node-waf configuration 88 | .lock-wscript 89 | 90 | # Compiled binary addons (https://nodejs.org/api/addons.html) 91 | build/Release 92 | 93 | # Dependency directories 94 | node_modules/ 95 | jspm_packages/ 96 | 97 | # Snowpack dependency directory (https://snowpack.dev/) 98 | web_modules/ 99 | 100 | # TypeScript cache 101 | *.tsbuildinfo 102 | 103 | # Optional npm cache directory 104 | .npm 105 | 106 | # Optional eslint cache 107 | .eslintcache 108 | 109 | # Optional stylelint cache 110 | .stylelintcache 111 | 112 | # Microbundle cache 113 | .rpt2_cache/ 114 | .rts2_cache_cjs/ 115 | .rts2_cache_es/ 116 | .rts2_cache_umd/ 117 | 118 | # Optional REPL history 119 | .node_repl_history 120 | 121 | # Output of 'npm pack' 122 | *.tgz 123 | 124 | # Yarn Integrity file 125 | .yarn-integrity 126 | 127 | # dotenv environment variable files 128 | .env 129 | .env.development.local 130 | .env.test.local 131 | .env.production.local 132 | .env.local 133 | 134 | # parcel-bundler cache (https://parceljs.org/) 135 | .cache 136 | .parcel-cache 137 | 138 | # Next.js build output 139 | .next 140 | out 141 | 142 | # Nuxt.js build / generate output 143 | .nuxt 144 | dist 145 | 146 | # Gatsby files 147 | .cache/ 148 | # Comment in the public line in if your project uses Gatsby and not Next.js 149 | # https://nextjs.org/blog/next-9-1#public-directory-support 150 | # public 151 | 152 | # vuepress build output 153 | .vuepress/dist 154 | 155 | # vuepress v2.x temp and cache directory 156 | .temp 157 | 158 | # Docusaurus cache and generated files 159 | .docusaurus 160 | 161 | # Serverless directories 162 | .serverless/ 163 | 164 | # FuseBox cache 165 | .fusebox/ 166 | 167 | # DynamoDB Local files 168 | .dynamodb/ 169 | 170 | # TernJS port file 171 | .tern-port 172 | 173 | # Stores VSCode versions used for testing VSCode extensions 174 | .vscode-test 175 | 176 | # yarn v2 177 | .yarn/cache 178 | .yarn/unplugged 179 | .yarn/build-state.yml 180 | .yarn/install-state.gz 181 | .pnp.* 182 | 183 | ### Node Patch ### 184 | # Serverless Webpack directories 185 | .webpack/ 186 | 187 | # Optional stylelint cache 188 | 189 | # SvelteKit build / generate output 190 | .svelte-kit 191 | 192 | ### VisualStudioCode ### 193 | .vscode/* 194 | !.vscode/settings.json 195 | !.vscode/tasks.json 196 | !.vscode/launch.json 197 | !.vscode/extensions.json 198 | !.vscode/*.code-snippets 199 | 200 | # Local History for Visual Studio Code 201 | .history/ 202 | 203 | # Built Visual Studio Code Extensions 204 | *.vsix 205 | 206 | ### VisualStudioCode Patch ### 207 | # Ignore all local history of files 208 | .history 209 | .ionide 210 | 211 | ### WebStorm+all ### 212 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 213 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 214 | 215 | # User-specific stuff 216 | .idea/**/workspace.xml 217 | .idea/**/tasks.xml 218 | .idea/**/usage.statistics.xml 219 | .idea/**/dictionaries 220 | .idea/**/shelf 221 | 222 | # AWS User-specific 223 | .idea/**/aws.xml 224 | 225 | # Generated files 226 | .idea/**/contentModel.xml 227 | 228 | # Sensitive or high-churn files 229 | .idea/**/dataSources/ 230 | .idea/**/dataSources.ids 231 | .idea/**/dataSources.local.xml 232 | .idea/**/sqlDataSources.xml 233 | .idea/**/dynamic.xml 234 | .idea/**/uiDesigner.xml 235 | .idea/**/dbnavigator.xml 236 | 237 | # Gradle 238 | .idea/**/gradle.xml 239 | .idea/**/libraries 240 | 241 | # Gradle and Maven with auto-import 242 | # When using Gradle or Maven with auto-import, you should exclude module files, 243 | # since they will be recreated, and may cause churn. Uncomment if using 244 | # auto-import. 245 | # .idea/artifacts 246 | # .idea/compiler.xml 247 | # .idea/jarRepositories.xml 248 | # .idea/modules.xml 249 | # .idea/*.iml 250 | # .idea/modules 251 | # *.iml 252 | # *.ipr 253 | 254 | # CMake 255 | cmake-build-*/ 256 | 257 | # Mongo Explorer plugin 258 | .idea/**/mongoSettings.xml 259 | 260 | # File-based project format 261 | *.iws 262 | 263 | # IntelliJ 264 | out/ 265 | 266 | # mpeltonen/sbt-idea plugin 267 | .idea_modules/ 268 | 269 | # JIRA plugin 270 | atlassian-ide-plugin.xml 271 | 272 | # Cursive Clojure plugin 273 | .idea/replstate.xml 274 | 275 | # SonarLint plugin 276 | .idea/sonarlint/ 277 | 278 | # Crashlytics plugin (for Android Studio and IntelliJ) 279 | com_crashlytics_export_strings.xml 280 | crashlytics.properties 281 | crashlytics-build.properties 282 | fabric.properties 283 | 284 | # Editor-based Rest Client 285 | .idea/httpRequests 286 | 287 | # Android studio 3.1+ serialized cache file 288 | .idea/caches/build_file_checksums.ser 289 | 290 | ### WebStorm+all Patch ### 291 | # Ignore everything but code style settings and run configurations 292 | # that are supposed to be shared within teams. 293 | 294 | .idea/* 295 | 296 | !.idea/codeStyles 297 | !.idea/runConfigurations 298 | 299 | ### Windows ### 300 | # Windows thumbnail cache files 301 | Thumbs.db 302 | Thumbs.db:encryptable 303 | ehthumbs.db 304 | ehthumbs_vista.db 305 | 306 | # Dump file 307 | *.stackdump 308 | 309 | # Folder config file 310 | [Dd]esktop.ini 311 | 312 | # Recycle Bin used on file shares 313 | $RECYCLE.BIN/ 314 | 315 | # Windows Installer files 316 | *.cab 317 | *.msi 318 | *.msix 319 | *.msm 320 | *.msp 321 | 322 | # Windows shortcuts 323 | *.lnk 324 | 325 | # End of https://www.toptal.com/developers/gitignore/api/macos,windows,linux,webstorm+all,visualstudiocode,node 326 | 327 | 328 | # 329 | # Local development-related files 330 | # 331 | .envrc 332 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | pids 2 | logs 3 | node_modules 4 | npm-debug.log 5 | coverage/ 6 | run 7 | dist 8 | .DS_Store 9 | .nyc_output 10 | .basement 11 | config.local.js 12 | basement_dist 13 | -------------------------------------------------------------------------------- /CONTRIBUTIONS.md: -------------------------------------------------------------------------------- 1 | # SQLAlchemy 가이드 문서에 기여하는 방법 2 | 3 | 먼저, 귀한 시간을 내어 SQLAlchemy 가이드에 기여해주셔서 감사합니다. 4 | 5 | 어떠한 형태로든지 이 프로젝트에 기여해주시는 것은 매우 감사한 일입니다. 이 문서를 통해 어떤 방법으로 이 프로젝트에 기여하실 수 있는지, 어떻게 관리되는지 살펴보세요. 이 프로젝트는 여러분들의 기여를 기다립니다. 🎉 6 | 7 | ## 목차 8 | 9 | ## 질문 있습니다 10 | 11 | > 질문하시는 내용은, 기본적으로 해당 [문서](/README.md)를 읽고 오셨음을 가정합니다. 12 | 13 | 질문하기 전에 도움이 될 만한 기존 [이슈](/issues)를 검색하는 것이 가장 좋습니다. 적절한 이슈를 찾았지만 여전히 설명이 필요한 경우 이 이슈에 질문을 작성할 수 있습니다. 또한 인터넷에서 먼저 답을 검색하는 것이 좋습니다. 14 | 15 | 그런 다음에도 여전히 질문이 필요하고 설명이 필요한 경우 다음을 권장합니다: 16 | 17 | - [새 이슈](/issues/new)를 엽니다. 18 | - 어떤 문제가 발생했는지 가능한 한 많은 컨텍스트를 제공해 주세요. 19 | - 관련성이 있다고 판단되는 경우 프로젝트 및 플랫폼 버전(`Node.js`, `yarn` 정보 등)을 알려주세요. 20 | 21 | 그러면 최대한 빨리 문제를 살펴볼 수 있습니다. 22 | 23 | ## 기여하고 싶어요 24 | 25 | > ### 법적 내용 고지 26 | > 27 | > 이 프로젝트에 기여한다는 것은 콘텐츠의 100%를 본인이 작성했으며, 콘텐츠에 필요한 권리를 보유하고 있으며, 기여한 콘텐츠가 프로젝트 라이선스에 따라 제공될 수 있다는 데 동의함을 의미합니다. 28 | 29 | ### 버그 리포팅 30 | 31 | #### 버그 리포트를 제출하기 전 32 | 33 | 좋은 버그 리포트는 다른 사람들이 더 많은 정보를 얻기 위해 여러분을 쫓아다녀야 하는 상황을 만들지 않아야 합니다. 따라서 신중하게 조사하고 정보를 수집한 후 버그 리포팅 문서에 문제를 자세히 설명해 주시기 바랍니다. 잠재적인 버그를 최대한 빨리 수정할 수 있도록 다음 단계를 미리 완료해 주세요. 34 | 35 | - 최신 버전을 사용하고 있는지 확인해 주세요. 36 | - 호환되지 않는 환경 구성 요소/버전을 사용하는 등 사용자 측의 오류가 아닌 실제 버그인지 확인합니다([문서](/README.md)를 읽어보시기 바랍니다). 추가적인 지원을 찾고 있다면 [이 섹션](#질문-있습니다)을 확인해 보세요). 37 | - 다른 사용자가 현재 겪고 있는 것과 동일한 문제를 경험했는지(이미 해결되었을 수도 있음) 확인하려면 [GitHub 이슈](/issues)에서 해당 버그나 오류에 대한 버그 리포트가 이미 존재하지 않는지 확인해 주세요. 38 | - 또한 인터넷(StackOverflow 포함)에서 검색하여 GitHub 커뮤니티 외부의 사용자가 해당 문제에 대해 논의했는지 확인해 주세요. 39 | - 버그에 대한 정보를 수집하세요: 40 | - 스택 추적(트레이스백) 41 | - OS, 플랫폼 및 버전(Windows, Linux, macOS, x86, ARM) 42 | - 관련성이 있다고 판단되는 경우 인터프리터, 컴파일러, SDK, 런타임 환경, 패키지 관리자 버전. 43 | - 입력 및 출력 44 | - 문제를 안정적으로 재현할 수 있나요? 그리고 이전 버전에서도 재현할 수 있나요? 45 | 46 | #### 버그 리포트는 어떻게 제출하나요? 47 | 48 | > 민감한 정보를 포함한 보안 관련 문제, 취약성 또는 버그를 이슈 트래커나 다른 공개적인 곳에 보고해서는 안 됩니다. 대신 민감한 버그는 [Slack 채널](https://join.slack.com/t/soogoonx2pythonists/shared_invite/zt-27rth6utw-8qibkZV4~TRXp8qosUniLQ)을 통해 보내주시기 바랍니다. 49 | 50 | 저희는 버그와 오류를 추적하기 위해 GitHub 이슈를 사용합니다. 프로젝트에 문제가 발생하면 다음과 같이 진행해주세요: 51 | 52 | - [이슈를 엽니다](/issues/new). (이 시점에서는 버그인지 아닌지 확신할 수 없으므로 아직 버그에 대해 이야기하지 말고 이슈에 레이블을 지정하지 마세요.) 53 | - 예상되는 동작과 실제 동작을 설명해 주세요. 54 | - 가능한 한 많은 컨텍스트를 제공하고 다른 사람이 직접 문제를 재현하기 위해 따를 수 있는 *재현 단계*를 설명해 주세요. 여기에는 일반적으로 코드가 포함됩니다. 좋은 버그 리포트를 작성하려면 문제를 분리하고 축소된 테스트 케이스를 만들어야 합니다. 55 | - 이전 섹션에서 수집한 정보를 제공해 주세요. 56 | 57 | 신고가 접수되면: 58 | 59 | - 프로젝트 팀에서 그에 따라 문제에 라벨을 붙입니다. 60 | - 이후 처리 사항에 따라 별도의 태그가 붙여집니다. 61 | 62 | ### 개선안 제안하기 63 | 64 | 이 섹션에서는 완전히 새로운 기능 및 기존 기능에 대한 사소한 개선 사항을 포함하여 `CONTRIBUTING.md` 에 대한 개선 제안을 제출하는 방법을 안내합니다. 이 가이드라인을 따르면 관리자와 커뮤니티가 제안을 이해하고 관련 제안을 찾는 데 도움이 됩니다. 65 | 66 | #### 개선안을 제안하시기 전 67 | 68 | - 최신 버전을 사용하고 있는지 확인해 주세요. 69 | - [문서](/README.md)를 주의 깊게 읽고 해당 기능이 이미 개별 설정에 포함되어 있는지 확인합니다. 70 | - [이슈 검색](/issues) 후, 개선 사항이 이미 제안되었는지 확인합니다. 이미 제안되었다면 새 이슈를 여는 대신 기존 이슈에 댓글을 추가해 주세요. 71 | - 여러분의 아이디어가 프로젝트의 범위와 목표에 맞는지 확인해 주세요. 프로젝트 개발자에게 이 기능의 장점을 설득할 수 있는 강력한 사례를 만드는 것은 여러분의 몫입니다. 일부 사용자가 아닌 대다수의 사용자에게 유용한 기능을 원한다는 점을 명심해 주세요. 소수의 사용자만 타겟팅하는 경우 애드온/플러그인 라이브러리를 작성하는 것을 고려해 주세요. 72 | 73 | #### 좋은 개선안을 제출하는 방법 74 | 75 | 개선안은 [GitHub issues](/issues) 로 트래킹 됩니다. 76 | 77 | - 제안을 식별할 수 있도록 이슈에 **명확하고 요점이 정리된 제목**을 사용해 주세요. 78 | - 제안된 개선 사항에 대한 **단계별 설명**을 최대한 자세하게 제공해 주세요. 79 | - 현재 동작을 **설명**하고 대신 어떤 동작이 표시되기를 기대했는지**와 그 이유를 설명해 주세요. 이 시점에서 어떤 대안이 효과가 없는지도 설명할 수 있습니다. 80 | - 단계를 설명하거나 제안과 관련된 부분을 지적하는 데 도움이 되는 스크린샷과 애니메이션 GIF를 **포함**할 수 있습니다. macOS 및 Windows에서는 [이 도구](https://www.cockos.com/licecap/)를, Linux에서는 [이 도구](https://github.com/colinkeenan/silentcast) 또는 [이 도구](https://github.com/GNOME/byzanz)를 사용하여 GIF를 녹화할 수 있습니다. 81 | - 이 개선 사항이 대부분의 CONTRIBUTING.md 사용자에게 **왜 유용한지** 설명해 주세요. 또한 이 문제를 더 잘 해결하고 영감을 줄 수 있는 다른 프로젝트에 대해서도 언급할 수 있습니다. 82 | 83 | ### 코드 기여방안 안내 84 | 85 | 코드 기여 방안은 아래와 같이 진행됩니다. 86 | 87 | 1. 이슈 생성 88 | 2. PR 리뷰 89 | 3. 머지 90 | 91 | ### 문서 기여방안 안내 92 | 93 | 문서를 개선하기 위해 로컬에서 `vuepress`를 구동하여 확인해볼 수 있습니다. 94 | 95 | 이를 위해 로컬에서 구동하는 방법을 기술합니다. 96 | 97 | #### 로컬 구동방법 98 | 99 | #### 사전 설치 100 | 101 | - Node.js 18 LTS 이상의 [Node.js](https://nodejs.org/en) 런타임 102 | - [(추천) `nvm`](https://github.com/nvm-sh/nvm) 103 | - [yarn](https://classic.yarnpkg.com/en/docs/install) 104 | 105 | #### 설치 방법 106 | 107 | 아래 명령어를 입력하여, 필요한 라이브러리를 모두 설치합니다. 108 | 109 | ```shell 110 | nvm install --lts=Hydrogen # 18.x 버전, 혹은 그 이상 111 | nvm use --lts=Hydrogen 112 | ``` 113 | 114 | ```shell 115 | npm install --global yarn # yarn 패키지 관리자 설치 116 | ``` 117 | 118 | ```shell 119 | yarn install # yarn 패키지 관리자를 이용한 라이브러리 설치 120 | ``` 121 | 122 | #### 구동 방법 123 | 124 | - 로컬 구동[^1] 125 | ```shell 126 | yarn dev 127 | ``` 128 | 129 | - 로컬 빌드 130 | ```shell 131 | yarn build 132 | ``` 133 | 134 | ## 스타일 가이드 135 | 136 | 이 구절에서는 본 프로젝트의 스타일 가이드에 대해 기술합니다. 137 | 138 | ### 커밋 메시지 139 | 140 | 아래 규칙을 지켜서 커밋 메시지를 작성해 주세요. 141 | 142 | - `[제목]` 은 영어로 작성합니다. 143 | - 커밋의 제목부분은 "_~한다_" 형식으로 기재합니다. 144 | 145 | 예시는 아래와 같습니다: 146 | 147 | ``` 148 | [tutorial] Section 1 내용을 추가한다 149 | ``` 150 | 151 | ## 프로젝트 팀 채널에 참여하세요 152 | 153 | README 페이지에 가이드된 Slack 워크스페이스로 오시면 됩니다([📎 슬랙 채널 참여](https://join.slack.com/t/soogoonx2pythonists/shared_invite/zt-27rth6utw-8qibkZV4~TRXp8qosUniLQ)). 154 | 155 | [^1]: `Node.js` 18.x의 경우, 아래 내용을 환경 변수로 담아주어야 정상작동을 합니다.
`export NODE_OPTIONS=--openssl-legacy-provider` ([참고 링크](https://github.com/SoogoonSoogoonPythonists/sqlalchemy-for-pythonist/pull/30#issue-2010749637)) 156 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sqlalchemy-for-pythonist 2 | 3 | 이 리포지토리는 SQLAlchemy 공식 문서를 잘 정리하여 쉽게 사용하게끔 가이드하는 문서와 관련 코드를 제공합니다. 4 | [VuePress 웹 페이지](https://soogoonsoogoonpythonists.github.io/sqlalchemy-for-pythonist/)에서 문서를 확인하실 수 있습니다. 5 | 6 | 현재 아래 하나의 문서를 정리한 상태입니다. 7 | 8 | - [Tutorial](src/tutorial/) 9 | - [기여 방법에 대한 문서](/CONTRIBUTIONS.md) 10 | 11 | 아래는 추후에 정리할 문서들입니다. 함께 참여해주시면 좋습니다! 12 | 13 | - SQLAlchemy Core 14 | - SQLAlchemy ORM 15 | - Dialect 16 | - Frequently Asked Questions 17 | 18 | 공식 문서를 정리하는 것 이외에, 본인이 직접 다듬어 정리하시는 글도 좋습니다 :) 19 | 20 | ## 커뮤니티 21 | 22 | 작업에 대한 소통을 좀 더 용이하게 하기 위해 슬랙 채널을 운영하고 있습니다. ([📎 슬랙 채널 참여](https://join.slack.com/t/soogoonx2pythonists/shared_invite/zt-27rth6utw-8qibkZV4~TRXp8qosUniLQ)) 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sqlalchemy-for-pythonist", 3 | "version": "0.0.1", 4 | "description": "SQLAlchemy를 쉽게 정리해놓은 문서입니다.", 5 | "main": "index.js", 6 | "authors": { 7 | "name": "", 8 | "email": "" 9 | }, 10 | "repository": "https://github.com/SoogoonSoogoonPythonists/sqlalchemy-for-pythonist/sqlalchemy-for-pythonist", 11 | "scripts": { 12 | "dev": "vuepress dev src", 13 | "build": "vuepress build src" 14 | }, 15 | "license": "MIT", 16 | "devDependencies": { 17 | "@vuepress/plugin-google-analytics": "^1.9.7", 18 | "@vuepress/plugin-last-updated": "^1.8.2", 19 | "vuepress": "^1.5.3", 20 | "vuepress-plugin-sitemap": "^2.3.1" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/.vuepress/components/Foo/Bar.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 16 | -------------------------------------------------------------------------------- /src/.vuepress/components/OtherComponent.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/.vuepress/components/demo-component.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 16 | -------------------------------------------------------------------------------- /src/.vuepress/config.js: -------------------------------------------------------------------------------- 1 | const { description } = require('../../package') 2 | 3 | module.exports = { 4 | base: "/sqlalchemy-for-pythonist/", 5 | 6 | // Path 7 | locales: { 8 | '/': { 9 | lang: 'ko', 10 | title: '파이썬 개발자를 위한 SQLAlchemy', 11 | description: description, 12 | }, 13 | '/en/': { 14 | lang: 'en-US', 15 | title: 'SQLAlchemy for Python Developers', 16 | description: 'This is a document that simplifies SQLAlchemy for easy understanding.', 17 | } 18 | }, 19 | 20 | /** 21 | * Extra tags to be injected to the page HTML `` 22 | * 23 | * ref:https://v1.vuepress.vuejs.org/config/#head 24 | */ 25 | head: [ 26 | ['meta', { name: 'theme-color', content: '#3eaf7c' }], 27 | ['meta', { name: 'apple-mobile-web-app-capable', content: 'yes' }], 28 | ['meta', { name: 'apple-mobile-web-app-status-bar-style', content: 'black' }], 29 | ['meta', { name: 'google-site-verification', content: 'wjX_mSoZBgO9SZMvjr96yOjo6n3_7pS8xNdmzDl1ESw' }], 30 | [ 31 | "script", 32 | { 33 | async: true, 34 | src: "https://www.googletagmanager.com/gtag/js?id=G-SNPCYHY4R2", 35 | }, 36 | ], 37 | ["script", {}, ["window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date()); gtag('config', 'G-SNPCYHY4R2');"]], 38 | ], 39 | 40 | /** 41 | * Theme configuration, here is the default theme configuration for VuePress. 42 | * 43 | * ref:https://v1.vuepress.vuejs.org/theme/default-theme-config.html 44 | */ 45 | themeConfig: { 46 | locales: { 47 | '/': { 48 | repo: '', 49 | editLinks: true, 50 | docsDir: '', 51 | editLinkText: '', 52 | lastUpdated: true, 53 | smoothScroll: true, 54 | nav: [ 55 | { 56 | text: 'GitHub', 57 | link: 'https://github.com/SoogoonSoogoonPythonists/sqlalchemy-for-pythonist' 58 | }, 59 | ], 60 | sidebar: { 61 | '/tutorial/': [ 62 | { 63 | title: 'Tutorial', 64 | path: '/tutorial/', 65 | collapsable: false, 66 | children: [ 67 | '1. 튜토리얼 개요', 68 | '2. 연결 설정하기', 69 | '3. 트랜잭션과 쿼리 실행하기', 70 | '4. 데이터베이스 메타데이터로 작업하기', 71 | '5.1. Core와 ORM 방식으로 행 조회하기', 72 | '5.2. Core 방식으로 행 삽입하기', 73 | '5.3. Core 방식으로 행 수정 및 삭제하기', 74 | '6. ORM 방식으로 데이터 조작하기', 75 | '7. ORM 방식으로 관련 개체 작업하기', 76 | ] 77 | }, 78 | ] 79 | } 80 | }, 81 | '/en/': { 82 | repo: '', 83 | editLinks: true, 84 | docsDir: '/en/', 85 | editLinkText: '/en/', 86 | lastUpdated: true, 87 | smoothScroll: true, 88 | nav: [ 89 | { 90 | text: 'GitHub', 91 | link: 'https://github.com/SoogoonSoogoonPythonists/sqlalchemy-for-pythonist' 92 | }, 93 | ], 94 | sidebar: { 95 | '/en/tutorial/': [ 96 | { 97 | title: 'Tutorial', 98 | path: '/en/tutorial/', 99 | collapsable: false, 100 | children: [ 101 | '1. Tutorial Overview', 102 | '2. Setting Up a Connection', 103 | '3. Executing Transactions and Queries', 104 | '4. Working with Database Metadata', 105 | '5.1. Querying Rows Using Core and ORM', 106 | '5.2. Inserting Rows Using Core', 107 | '5.3. Modifying and Deleting Rows Using Core', 108 | '6. Manipulating Data Using ORM', 109 | '7. Working with Related Objects Using ORM', 110 | ] 111 | }, 112 | ] 113 | } 114 | }, 115 | }, 116 | }, 117 | 118 | /** 119 | * Apply plugins,ref:https://v1.vuepress.vuejs.org/zh/plugin/ 120 | */ 121 | plugins: [ 122 | '@vuepress/plugin-back-to-top', 123 | '@vuepress/plugin-medium-zoom', 124 | ["sitemap", { hostname: "https://soogoonsoogoonpythonists.github.io/sqlalchemy-for-pythonist/" }], 125 | ["@vuepress/last-updated"], 126 | ], 127 | } 128 | -------------------------------------------------------------------------------- /src/.vuepress/enhanceApp.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Client app enhancement file. 3 | * 4 | * https://v1.vuepress.vuejs.org/guide/basic-config.html#app-level-enhancements 5 | */ 6 | 7 | export default ({ 8 | Vue, // the version of Vue being used in the VuePress app 9 | options, // the options for the root Vue instance 10 | router, // the router instance for the app 11 | siteData // site metadata 12 | }) => { 13 | // ...apply enhancements for the site. 14 | } 15 | -------------------------------------------------------------------------------- /src/.vuepress/styles/index.styl: -------------------------------------------------------------------------------- 1 | /** 2 | * Custom Styles here. 3 | * 4 | * ref:https://v1.vuepress.vuejs.org/config/#index-styl 5 | */ 6 | 7 | .home .hero img 8 | max-width 450px!important 9 | -------------------------------------------------------------------------------- /src/.vuepress/styles/palette.styl: -------------------------------------------------------------------------------- 1 | /** 2 | * Custom palette here. 3 | * 4 | * ref:https://v1.vuepress.vuejs.org/zh/config/#palette-styl 5 | */ 6 | 7 | $accentColor = #3eaf7c 8 | $textColor = #2c3e50 9 | $borderColor = #eaecef 10 | $codeBgColor = #282c34 11 | -------------------------------------------------------------------------------- /src/en/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | home: true 3 | heroImage: https://media.vlpt.us/images/zamonia500/post/6dd8b08b-a089-49db-a2f1-921ad6a9649e/connect-a-flask-app-to-a-mysql-database-with-sqlalchemy-and-pymysql.jpg 4 | tagline: This is a document that simplifies SQLAlchemy for easy understanding. 5 | actionText: Tutorial → 6 | actionLink: /en/tutorial/ 7 | footer: Made by soogoonsoogoon pythonists ❤️ 8 | --- 9 | -------------------------------------------------------------------------------- /src/en/tutorial/1. Tutorial Overview.md: -------------------------------------------------------------------------------- 1 | # Tutorial Overview 2 | 3 |
4 | 5 | ## Overview 6 | 7 | SQLAlchemy is a library in Python that facilitates the connection to databases and the use of ORM (Object-Relational Mapping). 8 | For instance, you can execute specific queries in your code and perform a series of operations in the database through ORM objects. 9 | 10 |
11 | 12 | ## Installation 13 | 14 | SQLAlchemy can be installed as follows: 15 | 16 | ```bash 17 | $ pip install sqlalchemy 18 | ``` 19 | 20 | The version being used is as follows: 21 | 22 | ```python 23 | >>> import sqlalchemy 24 | >>> sqlalchemy.__version__ 25 | 1.4.20 26 | ``` 27 | 28 |
29 | 30 | ## Offerings 31 | 32 | SQLAlchemy is offered in the following two ways: 33 | 34 | - **Core** 35 | - This is the database toolkit and the foundational architecture of SQLAlchemy. 36 | - It manages connections to databases, interacts with database queries and results, and provides tools to programmatically compose SQL statements. 37 | - **ORM** 38 | - Built on top of Core, it provides optional **ORM** (Object-Relational Mapping) features. 39 | 40 | It is generally recommended to understand Core first before using ORM. 41 | This tutorial will start by explaining Core. 42 | 43 | -------------------------------------------------------------------------------- /src/en/tutorial/2. Setting Up a Connection.md: -------------------------------------------------------------------------------- 1 | # Setting up a Connection 2 | 3 |
4 | 5 | ## Connecting to a Database 6 | 7 | Let's try connecting to SQLite, a relatively lightweight database. 8 | You can do it as follows: 9 | 10 | ```python 11 | >>> from sqlalchemy import create_engine 12 | >>> engine = create_engine("sqlite+pysqlite:///:memory:", echo=True, future=True) 13 | ``` 14 | 15 | - Use the `sqlalchemy.create_engine` function to create an **'engine'** that establishes a connection to the database. 16 | - The first argument is a **`string URL`**. 17 | - Typically, the `string URL` is structured as `dialect+driver://username:password@host:port/database`. 18 | - If you don't specify a `driver`, SQLAlchemy's default settings will be used. 19 | - Here, `sqlite+pysqlite:///test.db` is the `string URL`. 20 | - For `sqlite`, the format follows `sqlite:///`. 21 | - From the string URL `sqlite:///test.db`, we can understand the following information: 22 | - **Which database** to use (`dialect`, in this case, `sqlite`) 23 | - **Which database API** (the driver interacting with the database) to use (in this case, `pysqlite`) 24 | - **How to find** the database (in this case, it uses the in-memory feature provided by `sqlite`) 25 | - Setting the `echo` parameter to `True` prints all executed SQL. 26 | 27 | Creating an engine doesn't yet attempt an actual connection. The real connection occurs only when a request to perform an operation on the database is received for the first time. -------------------------------------------------------------------------------- /src/en/tutorial/3. Executing Transactions and Queries.md: -------------------------------------------------------------------------------- 1 | # Executing Transactions and Queries 2 | 3 |
4 | 5 | ## Obtaining connection 6 | 7 | You can connect to the database and execute a query as follows. 8 | 9 | ```python 10 | >>> from sqlalchemy import text 11 | 12 | >>> with engine.connect() as conn: 13 | ... result = conn.execute(text("select 'hello world'")) 14 | ... print(result.all()) 15 | 16 | [('hello world',)] 17 | ``` 18 | 19 | - Obtain a [`Connection`](https://docs.sqlalchemy.org/en/14/core/future.html#sqlalchemy.future.Connection) object through `engine.connect()` and store it in `conn`. 20 | - This `Connection` object allows you to interact with the database. 21 | - The `with` statement becomes a single transaction unit. 22 | - **Transactions are not committed automatically.** 23 | - You have to invoke the `Connection.commit()` to commit changes. 24 | 25 |
26 | 27 | ## Committing Changes 28 | 29 | Obtaining a connection, initiating a transaction, and interacting with the database **do not automatically commit** changes. 30 | 31 | To commit the change, you need to call `Connection.commit()` as follows. 32 | 33 | ```python 34 | >>> with engine.connect() as conn: 35 | ... # DDL - Creating the table 36 | ... conn.execute(text("CREATE TABLE some_table (x int, y int)")) 37 | ... # DML - Inserting data into the table 38 | ... conn.execute( 39 | ... text("INSERT INTO some_table (x, y) VALUES (:x, :y)"), 40 | ... [{"x": 1, "y": 1}, {"x": 2, "y": 4}] 41 | ... ) 42 | ... # TCL - Commiting changes. 43 | ... conn.commit() 44 | ``` 45 | 46 | When you run the code above, you'll see the following result below. 47 | 48 | ```sql 49 | BEGIN (implicit) 50 | CREATE TABLE some_table (x int, y int) 51 | [...] () 52 | 53 | INSERT INTO some_table (x, y) VALUES (?, ?) 54 | [...] ((1, 1), (2, 4)) 55 | 56 | COMMIT 57 | ``` 58 | 59 | You can also automatically commit at the end of a transaction using **`Engine.begin()`** and `with` statement. 60 | 61 | ```python 62 | >>> with engine.begin() as conn: 63 | ... conn.execute( 64 | ... text("INSERT INTO some_table (x, y) VALUES (:x, :y)"), 65 | ... [{"x": 6, "y": 8}, {"x": 9, "y": 10}] 66 | ... ) 67 | ... # Transaction commits automatically when the execution is done. 68 | ``` 69 | 70 | Executing the code above will yield the following results. 71 | 72 | ```sql 73 | BEGIN (implicit) 74 | INSERT INTO some_table (x, y) VALUES (?, ?) 75 | [...] ((6, 8), (9, 10)) 76 | 77 | COMMIT 78 | ``` 79 | 80 |
81 | 82 | ## Command Line Execution Basics 83 | 84 | You can execute queries and retrieve results as follows. 85 | 86 | ```python 87 | >>> with engine.connect() as conn: 88 | ... # conn.execute() initializes the result in an object named `result`. 89 | ... result = conn.execute(text("SELECT x, y FROM some_table")) 90 | ... for row in result: 91 | ... print(f"x: {row.x} y: {row.y}") 92 | 93 | x: 1 y: 1 94 | x: 2 y: 4 95 | x: 6 y: 8 96 | x: 9 y: 10 97 | ``` 98 | 99 | - The [`Result`](https://docs.sqlalchemy.org/en/14/core/connections.html#sqlalchemy.engine.Result) object is the object that **holds the "query result"** returned by `conn.execute()`. 100 | - You can see what features it provides by clicking on the link. 101 | - For instance, you can receive _a list_ of Row objects using `Result.all()`. 102 | 103 | > cf. Both `Result` and `Row` are objects provided by SQLAlchemy. 104 | 105 | You can access each row using the `Result` object as follows. 106 | 107 | ```python 108 | result = conn.execute(text("select x, y from some_table")) 109 | 110 | # Accessing the tuple. 111 | for x, y in result: 112 | # ... 113 | 114 | # Accessing the value by using integer index. 115 | for row in result: 116 | x = row[0] 117 | 118 | # Accessing the value by using a name of the property. 119 | for row in result: 120 | y = row.y 121 | 122 | # Accessing the value by using a mapping access. 123 | for dict_row in result.mappings(): 124 | x = dict_row['x'] 125 | y = dict_row['y'] 126 | ``` 127 | 128 |
129 | 130 | ## Passing parameters to your query 131 | 132 | You can pass a parameter to a query as follows. 133 | 134 | ```python 135 | >>> with engine.connect() as conn: 136 | ... result = conn.execute( 137 | ... text("SELECT x, y FROM some_table WHERE y > :y"), # Receive in colon format (`:`). 138 | ... {"y": 2} # Pass by `dict`. 139 | ... ) 140 | ... for row in result: 141 | ... print(f"x: {row.x} y: {row.y}") 142 | 143 | x: 2 y: 4 144 | x: 6 y: 8 145 | x: 9 y: 10 146 | ``` 147 | 148 | You can also send multiple parameters like this. 149 | 150 | ```python 151 | >>> with engine.connect() as conn: 152 | ... conn.execute( 153 | ... text("INSERT INTO some_table (x, y) VALUES (:x, :y)"), 154 | ... [{"x": 11, "y": 12}, {"x": 13, "y": 14}] # Pass by `List[dict]`` 155 | ... ) 156 | ... conn.commit() 157 | ``` 158 | 159 | The above code executes the following query. 160 | 161 | ```sql 162 | INSERT INTO some_table (x, y) VALUES (?, ?) [...] ((11, 12), (13, 14)) 163 | ``` 164 | 165 |
166 | 167 | ## Executing ORM by using `Session` 168 | 169 | From now on, let's execute the query using `Session` provided by the `ORM`, instead of the `Connection` object. 170 | You can do it as follows: 171 | 172 | ```python 173 | >>> from sqlalchemy.orm import Session 174 | 175 | >>> stmt = text("SELECT x, y FROM some_table WHERE y > :y ORDER BY x, y").bindparams(y=6) 176 | 177 | >>> # Pass an instance of the Engine object to the Session object 178 | >>> # to get an instance that can interact with the database. 179 | >>> with Session(engine) as session: 180 | ... # Executing the query using Session.execute(). 181 | ... result = session.execute(stmt) 182 | ... for row in result: 183 | ... print(f"x: {row.x} y: {row.y}") 184 | ``` 185 | 186 | Like `Connection`, `Session` also **does not automatically commit** upon closing. To commit, you need to _explicitly call_ `Session.commit()` as follows: 187 | 188 | ```python 189 | >>> with Session(engine) as session: 190 | ... result = session.execute( 191 | ... text("UPDATE some_table SET y=:y WHERE x=:x"), 192 | ... [{"x": 9, "y":11}, {"x": 13, "y": 15}] 193 | ... ) 194 | ... # You have to call `commit()` explicitly. 195 | ... session.commit() 196 | ``` 197 | -------------------------------------------------------------------------------- /src/en/tutorial/4. Working with Database Metadata.md: -------------------------------------------------------------------------------- 1 | # Working with Database Metadata 2 | 3 |
4 | 5 | SQLAlchemy Core and ORM were created to allow Python objects to be used like tables and columns in a database. These can be used as _database metadata_. 6 | 7 | > Metadata describes data that describes other data. Here, **metadata refers to configured tables, columns, constraints, and other object information**. 8 | 9 |
10 | 11 | ## Creating a table object and add it to your metadata 12 | 13 | In relational databases, tables are created via queries, but in SQLAlchemy, tables can be created through Python objects. 14 | To start with SQLAlchemy Expression Language, you need to create a `Table` object for the database table you want to use. 15 | 16 | ```python 17 | >>> from sqlalchemy import MetaData 18 | >>> # An object that will hold the meta information for the tables. 19 | >>> metadata = MetaData() 20 | >>> 21 | >>> from sqlalchemy import Table, Column, Integer, String 22 | >>> user_table = Table( 23 | ... # The name of the table that will be stored in the database. 24 | ... 'user_account', 25 | ... metadata, 26 | ... 27 | ... # The columns that will go into this table. 28 | ... Column('id', Integer, primary_key=True), 29 | ... Column('name', String(30)), 30 | ... Column('fullname', String), 31 | ... ) 32 | ``` 33 | 34 | - You can create database tables using the `Table` object. 35 | - Columns of the table are implemented using `Column`. 36 | - By default, it defines like `Column(column name, data type)`. 37 | 38 | - After creating a `Table` instance, you can know the created column information as follows: 39 | 40 | ```Python 41 | >>> user_table.c.name 42 | Column('name', String(length=30), table=) 43 | 44 | >>> user_table.c.keys() 45 | ['id', 'name', 'fullname'] 46 | ``` 47 | 48 |
49 | 50 | ## Declaring Simple Constraints 51 | 52 | We saw the `Column('id', Integer, primary_key=True)` statement in the code that creates the user table above. This declares the id column as the primary key. 53 | 54 | The primary key is implicitly declared as a structure in the `PrimaryKeyConstraint` object. This can be confirmed as follows. 55 | 56 | ```python 57 | >>> user_table.primary_key 58 | PrimaryKeyConstraint(Column('id', Integer(), table=, primary_key=True, nullable=False)) 59 | ``` 60 | 61 | Along with the primary key, foreign keys can also be declared as follows. 62 | 63 | ```python 64 | >>> from sqlalchemy import ForeignKey 65 | >>> address_table = Table( 66 | ... "address", 67 | ... metadata, 68 | ... Column('id', Integer, primary_key=True), 69 | ... # Declaring Foreign Key as `ForeignKey` object. 70 | ... Column('user_id', ForeignKey('user_account.id'), nullable=False), 71 | ... Column('email_address', String, nullable=False) 72 | ... ) 73 | ``` 74 | 75 | - You can declare a foreign key column in the form of `ForeignKey('table_name.foreign_key')`. 76 | - In this case, you can omit the data type of the `Column` object. The data type is automatically inferred by locating the column corresponding to the foreign key. 77 | - You can also declare a `NOT NULL` constraint on a column by passing the `nullable=False` parameter and value. 78 | 79 |
80 | 81 | ## Applying to your database 82 | 83 | We have declared database tables using SQLAlchemy so far. Now, let's make these declared tables actually get created in the database. 84 | 85 | Execute `metadata.create_all()` as follows. 86 | 87 | ```python 88 | >>> metadata.create_all(engine) 89 | 90 | # The above code creates all tables recorded in the metadata instance. 91 | # As a result, it executes the following queries. 92 | 93 | BEGIN (implicit) 94 | PRAGMA main.table_...info("user_account") 95 | ... 96 | PRAGMA main.table_...info("address") 97 | ... 98 | CREATE TABLE user_account ( 99 | id INTEGER NOT NULL, 100 | name VARCHAR(30), 101 | fullname VARCHAR, 102 | PRIMARY KEY (id) 103 | ) 104 | ... 105 | CREATE TABLE address ( 106 | id INTEGER NOT NULL, 107 | user_id INTEGER NOT NULL, 108 | email_address VARCHAR NOT NULL, 109 | PRIMARY KEY (id), 110 | FOREIGN KEY(user_id) REFERENCES user_account (id) 111 | ) 112 | ... 113 | COMMIT 114 | ``` 115 | 116 |
117 | 118 | ## Defining table metadata the ORM way 119 | 120 | We will create the same database structure and use the same constraints as above, but this time we will proceed using the ORM approach. 121 | 122 |
123 | 124 | ### Setting the `registry` object. 125 | 126 | First of all, create a `registry` object as follows. 127 | 128 | ```python 129 | >>> from sqlalchemy.orm import registry 130 | >>> mapper_registry = registry() 131 | ``` 132 | 133 | The `registry` object contains a `MetaData` object. 134 | 135 | ```python 136 | >>> mapper_registry.metadata 137 | MetaData() 138 | ``` 139 | 140 | Now we can execute the following code. 141 | 142 | ```python 143 | >>> Base = mapper_registry.generate_base() 144 | ``` 145 | 146 | > The above process can be simplified using `declarative_base` as follows. 147 | > 148 | > ```python 149 | > >>> from sqlalchemy.orm import declarative_base 150 | > >>> Base = declarative_base() 151 | > ``` 152 | 153 |
154 | 155 | ### Declaring the ORM object 156 | 157 | By defining a subclass that inherits from the `Base` object, you can declare tables in the database using the ORM approach. 158 | 159 | ```python 160 | >>> from sqlalchemy.orm import relationship 161 | >>> class User(Base): 162 | ... # A name of the table to be used in the database. 163 | ... __tablename__ = 'user_account' 164 | ... 165 | ... id = Column(Integer, primary_key=True) 166 | ... name = Column(String(30)) 167 | ... fullname = Column(String) 168 | ... 169 | ... addresses = relationship("Address", back_populates="user") 170 | ... 171 | ... def __repr__(self): 172 | ... return f"User(id={self.id!r}, name={self.name!r}, fullname={self.fullname!r})" 173 | 174 | >>> class Address(Base): 175 | ... __tablename__ = 'address' 176 | ... 177 | ... id = Column(Integer, primary_key=True) 178 | ... email_address = Column(String, nullable=False) 179 | ... user_id = Column(Integer, ForeignKey('user_account.id')) 180 | ... 181 | ... user = relationship("User", back_populates="addresses") 182 | ... 183 | ... def __repr__(self): 184 | ... return f"Address(id={self.id!r}, email_address={self.email_address!r})" 185 | ``` 186 | 187 | The `User` and `Address` objects include a `Table` object. 188 | 189 | You can check this through the `__table__` attribute as follows. 190 | 191 | ```python 192 | >>> User.__table__ 193 | Table('user_account', MetaData(), 194 | Column('id', Integer(), table=, primary_key=True, nullable=False), 195 | Column('name', String(length=30), table=), 196 | Column('fullname', String(), table=), schema=None) 197 | ``` 198 | 199 |
200 | 201 | ### Creating an ORM object 202 | 203 | After defining the table, you can create an ORM object as follows. 204 | 205 | ```python 206 | >>> sandy = User(name="sandy", fullname="Sandy Cheeks") 207 | >>> sandy 208 | User(id=None, name='sandy', fullname='Sandy Cheeks') 209 | ``` 210 | 211 |
212 | 213 | ### Applying to your database 214 | 215 | Now, you can apply the tables declared with ORM to the actual database as follows. 216 | 217 | ```python 218 | >>> mapper_registry.metadata.create_all(engine) 219 | >>> Base.metadata.create_all(engine) 220 | ``` 221 | 222 |
223 | 224 | ## Importing tables from an existing database into an ORM object 225 | 226 | Aside from the above methods, there is a way to retrieve tables from the database without declaring them directly. 227 | 228 | ```python 229 | >>> some_table = Table("some_table", metadata, autoload_with=engine) 230 | 231 | BEGIN (implicit) 232 | PRAGMA main.table_...info("some_table") 233 | [raw sql] () 234 | SELECT sql FROM (SELECT * FROM sqlite_master UNION ALL SELECT * FROM sqlite_temp_master) WHERE name = ? AND type = 'table' 235 | [raw sql] ('some_table',) 236 | PRAGMA main.foreign_key_list("some_table") 237 | ... 238 | PRAGMA main.index_list("some_table") 239 | ... 240 | ROLLBACK 241 | ``` 242 | 243 | Now it can be used as follows: 244 | 245 | ```python 246 | >>> some_table 247 | Table('some_table', MetaData(), 248 | Column('x', INTEGER(), table=), 249 | Column('y', INTEGER(), table=), 250 | schema=None) 251 | ``` 252 | -------------------------------------------------------------------------------- /src/en/tutorial/5.2. Inserting Rows Using Core.md: -------------------------------------------------------------------------------- 1 | # Inserting Rows Using Core 2 | 3 | In this chapter, we learn how to INSERT data using the SQLAlchemy Core approach. 4 | 5 |
6 | 7 | ## Constructing SQL Expressions with `insert()` 8 | 9 | First, you can create an INSERT statement like this: 10 | 11 | ```python 12 | >>> from sqlalchemy import insert 13 | 14 | # stmt is an instance of the Insert object. 15 | >>> stmt = insert(user_table).values(name='spongebob', fullname="Spongebob Squarepants") 16 | >>> print(stmt) 17 | 'INSERT INTO user_account (name, fullname) VALUES (:name, :fullname)' 18 | ``` 19 | 20 | > Here, user_table is the Table object we created in the previous chapter. We created it as follows. 21 | > 22 | > ```python 23 | > from sqlalchemy import MetaData 24 | > from sqlalchemy import Table, Column, Integer, String 25 | > 26 | > metadata = MetaData() 27 | > user_table = Table( 28 | > 'user_account', 29 | > metadata, 30 | > Column('id', Integer, primary_key=True), 31 | > Column('name', String(30)), 32 | > Column('fullname', String), 33 | > ) 34 | > ``` 35 | 36 | Looking at `stmt`, you'll notice that the parameters have not yet been mapped. 37 | This can be checked after `compile()` it, as shown next. 38 | 39 | ```python 40 | >>> compiled = stmt.compile() 41 | >>> print(compiled.params) 42 | {'name': 'spongebob', 'fullname': 'Spongebob Squarepants'} 43 | ``` 44 | 45 |
46 | 47 | ## Executing the Statement 48 | 49 | Now, let's execute the INSERT statement we created above using the Core approach. 50 | 51 | ```python 52 | >>> with engine.connect() as conn: 53 | ... result = conn.execute(stmt) 54 | ... conn.commit() 55 | 56 | # The above code executes the following query. 57 | 58 | BEGIN (implicit) 59 | INSERT INTO user_account (name, fullname) VALUES (?, ?) 60 | [...] ('spongebob', 'Spongebob Squarepants') 61 | COMMIT 62 | ``` 63 | 64 | What information does the result contain, which is obtained from the return value of `conn.execute(stmt)`? 65 | result is a [`CursorResult`](https://docs.sqlalchemy.org/en/20/core/connections.html#sqlalchemy.engine.CursorResult) object. 66 | It holds various information about the execution results, particularly the [`Row`](https://docs.sqlalchemy.org/en/20/core/connections.html#sqlalchemy.engine.Row) objects that contain data rows. 67 | 68 | Since we have just inserted data, we can check the primary key value of the inserted data as follows. 69 | 70 | ```python 71 | >>> result.inserted_primary_key # This is also a Row object. 72 | (1, ) # As the primary key can be composed of multiple columns, it is represented as a tuple. 73 | ``` 74 | 75 |
76 | 77 | ## Passing INSERT Parameters to `Connection.execute()` 78 | 79 | Above, we created a statement that included `values` along with `insert`. 80 | 81 | ```python 82 | >>> stmt = insert(user_table).values(name='spongebob', fullname="Spongebob Squarepants") 83 | ``` 84 | 85 | However, besides this method, you can also execute an INSERT statement by passing parameters to the `Connection.execute()` method. The official documentation suggests this as a more common approach. 86 | 87 | ```python 88 | >>> with engine.connect() as conn: 89 | ... result = conn.execute( 90 | ... insert(user_table), 91 | ... [ 92 | ... {"name": "sandy", "fullname": "Sandy Cheeks"}, 93 | ... {"name": "patrick", "fullname": "Patrick Star"} 94 | ... ] 95 | ... ) 96 | ... conn.commit() 97 | ``` 98 | 99 | > The official documentation also explains how to execute statements including subqueries in a separate section. However, it has been deemed not entirely suitable for the tutorial content and is not included in this text. 100 | > For those interested in this topic, please refer to the [original documentation](https://docs.sqlalchemy.org/en/20/tutorial/data_insert.html#insert-usually-generates-the-values-clause-automatically). 101 | 102 |
103 | 104 | ## `Insert.from_select()` 105 | 106 | Sometimes you need a query to INSERT rows that are received from a SELECT statement, as in the following example. 107 | 108 | Such cases can be written as shown in the following code. 109 | 110 | ```python 111 | >>> select_stmt = select(user_table.c.id, user_table.c.name + "@aol.com") 112 | >>> insert_stmt = insert(address_table).from_select( 113 | ... ["user_id", "email_address"], select_stmt 114 | ... ) 115 | >>> print(insert_stmt) 116 | """ 117 | INSERT INTO address (user_id, email_address) 118 | SELECT user_account.id, user_account.name || :name_1 AS anon_1 119 | FROM user_account 120 | """ 121 | ``` 122 | 123 |
124 | 125 | ## `Insert.returning()` 126 | 127 | There are situations where you need to receive the value of the processed rows from the database after query processing. This is known as the RETURNING syntax. 128 | For an introduction to this, it would be good to read [this wiki](https://wiki.postgresql.org/wiki/UPSERT). 129 | 130 | In SQLAlchemy Core, this `RETURNING` syntax can be written as follows. 131 | 132 | ```python 133 | >>> insert_stmt = insert(address_table).returning(address_table.c.id, address_table.c.email_address) 134 | >>> print(insert_stmt) 135 | """ 136 | INSERT INTO address (id, user_id, email_address) 137 | VALUES (:id, :user_id, :email_address) 138 | RETURNING address.id, address.email_address 139 | """ 140 | ``` 141 | -------------------------------------------------------------------------------- /src/en/tutorial/5.3. Modifying and Deleting Rows Using Core.md: -------------------------------------------------------------------------------- 1 | # Modifying and Deleting Rows Using Core 2 | 3 | In this chapter, we explain the Update and Delete statements used for modifying and deleting existing rows using the Core approach in SQLAlchemy. 4 | 5 |
6 | 7 | ## Constructing SQL Expressions with `update()` 8 | 9 | You can write an UPDATE statement as follows. 10 | 11 | ```python 12 | >>> from sqlalchemy import update 13 | >>> stmt = ( 14 | ... update(user_table).where(user_table.c.name == 'patrick'). 15 | ... values(fullname='Patrick the Star') 16 | ... ) 17 | >>> print(stmt) 18 | 'UPDATE user_account SET fullname=:fullname WHERE user_account.name = :name_1' 19 | ``` 20 | 21 | ```python 22 | >>> stmt = ( 23 | ... update(user_table). 24 | ... values(fullname="Username: " + user_table.c.name) 25 | ... ) 26 | >>> print(stmt) 27 | 'UPDATE user_account SET fullname=(:name_1 || user_account.name)' 28 | ``` 29 | 30 | > The original text discusses `bindparam()`, but since I haven't seen many use cases for it, it is omitted in this text. If you're curious, please refer to [the original content](https://docs.sqlalchemy.org/en/20/tutorial/data_update.html). 31 | 32 |
33 | 34 | ### Correlated Update 35 | 36 | Using a [Correlated Subquery](https://docs.sqlalchemy.org/en/20/tutorial/data_select.html#tutorial-scalar-subquery), you can utilize rows from another table as follows. 37 | 38 | ```python 39 | >>> scalar_subq = ( 40 | ... select(address_table.c.email_address). 41 | ... where(address_table.c.user_id == user_table.c.id). 42 | ... order_by(address_table.c.id). 43 | ... limit(1). 44 | ... scalar_subquery() 45 | ... ) 46 | >>> update_stmt = update(user_table).values(fullname=scalar_subq) 47 | >>> print(update_stmt) 48 | """ 49 | UPDATE user_account SET fullname=(SELECT address.email_address 50 | FROM address 51 | WHERE address.user_id = user_account.id ORDER BY address.id 52 | LIMIT :param_1) 53 | """ 54 | ``` 55 | 56 |
57 | 58 | ### Updating with Conditions Related to Another Table 59 | 60 | When updating a table, there are times when you need to set conditions in relation to information from another table. 61 | In such cases, you can use it as shown in the example below. 62 | 63 | ```python 64 | >>> update_stmt = ( 65 | ... update(user_table). 66 | ... where(user_table.c.id == address_table.c.user_id). 67 | ... where(address_table.c.email_address == 'patrick@aol.com'). 68 | ... values(fullname='Pat') 69 | ... ) 70 | >>> print(update_stmt) 71 | """ 72 | UPDATE user_account SET fullname=:fullname FROM address 73 | WHERE user_account.id = address.user_id AND address.email_address = :email_address_1 74 | """ 75 | ``` 76 | 77 |
78 | 79 | ### Updating Multiple Tables Simultaneously 80 | 81 | You can simultaneously update specific values in multiple tables that meet certain conditions, as shown in the following example. 82 | 83 | ```python 84 | >>> update_stmt = ( 85 | ... update(user_table). 86 | ... where(user_table.c.id == address_table.c.user_id). 87 | ... where(address_table.c.email_address == 'patrick@aol.com'). 88 | ... values( 89 | ... { 90 | ... user_table.c.fullname: "Pat", 91 | ... address_table.c.email_address: "pat@aol.com" 92 | ... } 93 | ... ) 94 | ... ) 95 | >>> from sqlalchemy.dialects import mysql 96 | >>> print(update_stmt.compile(dialect=mysql.dialect())) 97 | """ 98 | UPDATE user_account, address 99 | SET address.email_address=%s, user_account.fullname=%s 100 | WHERE user_account.id = address.user_id AND address.email_address = %s 101 | """ 102 | ``` 103 | 104 | > I did not include a summary of the '[Parameter Ordered Updates](https://docs.sqlalchemy.org/en/20/tutorial/data_update.html#parameter-ordered-updates)' section from the original text because I did not understand it. 105 | > If someone understands this part well, it would be appreciated if you could contribute to this document. 106 | 107 |
108 | 109 | ## Constructing SQL Expressions with `delete()` 110 | 111 | You can write a DELETE statement as follows. 112 | 113 | ```python 114 | >>> from sqlalchemy import delete 115 | >>> stmt = delete(user_table).where(user_table.c.name == 'patrick') 116 | >>> print(stmt) 117 | """ 118 | DELETE FROM user_account WHERE user_account.name = :name_1 119 | """ 120 | ``` 121 | 122 |
123 | 124 | ### Deleting with a JOIN to Another Table 125 | 126 | There are cases where you need to delete data that meets specific conditions after joining with another table. (If this is unclear, refer to [this article](https://stackoverflow.com/questions/11366006/mysql-join-on-vs-using) for clarification.) 127 | In such cases, you can use it as shown in the example below. 128 | 129 | ```python 130 | >>> delete_stmt = ( 131 | ... delete(user_table). 132 | ... where(user_table.c.id == address_table.c.user_id). 133 | ... where(address_table.c.email_address == 'patrick@aol.com') 134 | ... ) 135 | >>> from sqlalchemy.dialects import mysql 136 | >>> print(delete_stmt.compile(dialect=mysql.dialect())) 137 | """ 138 | DELETE FROM user_account USING user_account, address 139 | WHERE user_account.id = address.user_id AND address.email_address = %s 140 | """ 141 | ``` 142 | 143 |
144 | 145 | ## Getting the Number of Rows Affected in UPDATE, DELETE 146 | 147 | You can obtain the number of rows processed by a query using the ['Result.rowcount'](https://docs.sqlalchemy.org/en/20/core/connections.html#sqlalchemy.engine.CursorResult.rowcount) property, as shown next. 148 | 149 | ```python 150 | >>> with engine.begin() as conn: 151 | ... result = conn.execute( 152 | ... update(user_table). 153 | ... values(fullname="Patrick McStar"). 154 | ... where(user_table.c.name == 'patrick') 155 | ... ) 156 | ... print(result.rowcount) # You can use the rowcount property of the Result object. 157 | 158 | 1 # The number of rows processed by the query (the same as the number of rows matching the conditions). 159 | ``` 160 | 161 |
162 | 163 | ## Using RETURNING with UPDATE, DELETE 164 | 165 | You can use the RETURNING syntax as follows. 166 | 167 | For more on the RETURNING syntax, please see [this article](https://www.postgresql.org/docs/current/dml-returning.html). 168 | 169 | ```python 170 | >>> update_stmt = ( 171 | ... update(user_table).where(user_table.c.name == 'patrick'). 172 | ... values(fullname='Patrick the Star'). 173 | ... returning(user_table.c.id, user_table.c.name) 174 | ... ) 175 | >>> print(update_stmt) 176 | """ 177 | UPDATE user_account SET fullname=:fullname 178 | WHERE user_account.name = :name_1 179 | RETURNING user_account.id, user_account.name 180 | """ 181 | ``` 182 | 183 | ```python 184 | >>> delete_stmt = ( 185 | ... delete(user_table).where(user_table.c.name == 'patrick'). 186 | ... returning(user_table.c.id, user_table.c.name) 187 | ... ) 188 | >>> print(delete_stmt) 189 | """ 190 | DELETE FROM user_account 191 | WHERE user_account.name = :name_1 192 | RETURNING user_account.id, user_account.name 193 | """ 194 | ``` -------------------------------------------------------------------------------- /src/en/tutorial/6. Manipulating Data Using ORM.md: -------------------------------------------------------------------------------- 1 | # Manipulating Data Using ORM 2 | 3 | Until the previous chapter, we focused on utilizing queries from the `CORE` perspective. 4 | 5 | In this chapter, we explain the components, lifecycle, and interaction methods of the `Session` used in the ORM approach. 6 | 7 |
8 | 9 | ## Inserting rows with ORM 10 | 11 | The `Session` object, when using ORM, creates Insert objects and emits them in transactions. `Session` adds object entries to perform these processes. Then, through a process called `flush`, it records the new items in the database. 12 | 13 | ### Instances of Objects Representing Rows 14 | 15 | In the previous process, we executed INSERT using a Python Dictionary. 16 | 17 | In ORM, we directly use user-defined Python objects defined in the table metadata definition. 18 | 19 | ```python 20 | >>> squidward = User(name="squidward", fullname="Squidward Tentacles") 21 | >>> krabs = User(name="ehkrabs", fullname="Eugene H. Krabs") 22 | ``` 23 | 24 | We create two `User` objects that represent potential database rows to be INSERTed. Because of `__init__()` constructor automatically created by ORM mapping, we can create each object using the constructor's column names as keys. 25 | 26 | ```python 27 | >>> squidward 28 | User(id=None, name='squidward', fullname='Squidward Tentacles') 29 | ``` 30 | 31 | Similar to Core's `Insert`, ORM integrates it even without including a primary key. The `None` value for `id` is provided by SQLAlchemy to indicate that the attribute does not have a value yet. 32 | 33 | Currently, the two objects (`squiward` and `krabs`) are in a state called `transient`. The `transient` state means they are not yet connected to any database and not yet associated with a `Session` object that can generate an `INSERT` statement. 34 | 35 | ### Adding Objects to the `Session` 36 | 37 | ```python 38 | >>> session = Session(engine) # It is essential to close after use. 39 | >>> session.add(squidward) # Insert an object into session via Session.add() method. 40 | >>> session.add(krabs) 41 | ``` 42 | 43 | When an object is added to the `Session` through `Session.add()`, it is called being in the `pending` state. 44 | The pending state means the object has not yet been added to the database. 45 | 46 | ```python 47 | >>> session.new # You can check the objects in the pending state through session.new. Objects are added to the Session using the Session.add() method. 48 | IdentitySet([User(id=None, name='squidward', fullname='Squidward Tentacles'), User(id=None, name='ehkrabs', fullname='Eugene H. Krabs')]) 49 | ``` 50 | 51 | - `IdentitySet` is a Python set that hashes object IDs in all cases. 52 | - That is, it uses the `id()` method, not the `hash()` function of Python's built-in functions." 53 | 54 | ### Flushing 55 | 56 | The `Session` object uses the [unit of work pattern](https://martinfowler.com/eaaCatalog/unitOfWork.html). This means that it accumulates changes but does not actually communicate with the database until necessary. 57 | This behavior allows objects in the previously mentioned `pending` state to be used more efficiently in SQL DML. 58 | The process of actually sending the current changes to the Database via SQL is called flushing. 59 | 60 | ```python 61 | >>> session.flush() 62 | """ 63 | INSERT INTO user_account (name, fullname) VALUES (?, ?) 64 | [...] ('squidward', 'Squidward Tentacles') 65 | INSERT INTO user_account (name, fullname) VALUES (?, ?) 66 | [...] ('ehkrabs', 'Eugene H. Krabs') 67 | """ 68 | ``` 69 | 70 | Now, the transaction remains open until one of `Session.commit()`, `Session.rollback()`, or `Session.close()` is invoked. 71 | 72 | While you can use `Session.flush()` directly to push the current pending contents, `Session` typically features `autoflush`, so this is usually not necessary. `Session.commit()` flushes changes every time it is called. 73 | 74 | ### Automatically Generated Primary Key Properties 75 | 76 | When a row is inserted, the Python object we created becomes `persistent`. 77 | The `persistent` state is associated with the loaded `Session` object. 78 | 79 | During `INSERT`, the ORM retrieves the primary key identifier for each new object. 80 | This uses the same `CursorResult.inserted_primary_key` accessor introduced earlier. 81 | 82 | ```python 83 | >>> squidward.id 84 | 4 85 | >>> krabs.id 86 | 5 87 | ``` 88 | 89 | > When ORM is flushed, instead of `executemany`, two separate INSERT statements are used because of this `CursorResult.inserted_primary_key`. 90 | > In SQLite, for instance, you need to `INSERT` one column at a time to use the auto-increment feature (other various databases like PostgreSQL's IDENTITY or SERIAL function similarly). 91 | > If a database connection like `psycopg2`, which can provide primary key information for many rows at once, is used, the ORM optimizes this to `INSERT` many rows at once." 92 | 93 | ### Identity Map 94 | 95 | `Identity Map` (`ID Map`) is an in-memory storage that links all objects currently loaded in memory to their primary key IDs. You can retrieve one of these objects through `Session.get()`. This method searches for the object in the `ID Map` if it's in memory, or through a `SELECT` statement if it's not. 96 | 97 | ```python 98 | >>> some_squidward = session.get(User, 4) 99 | >>> some_squidward 100 | User(id=4, name='squidward', fullname='Squidward Tentacles') 101 | ``` 102 | 103 | An important point is that the `ID Map` maintains unique objects among Python objects. 104 | 105 | ```python 106 | >>> some_squidward is squidward 107 | True 108 | ``` 109 | 110 | The `ID Map` is a crucial feature that allows manipulation of complex object sets within a transaction in an unsynchronized state. 111 | 112 | ### Committing 113 | 114 | We now `commit` the current changes to the transaction. 115 | 116 | ```python 117 | >>> session.commit() 118 | COMMIT 119 | ``` 120 | 121 |
122 | 123 | ## How to UPDATE ORM objects 124 | 125 | There are two ways to perform an `UPDATE` through ORM: 126 | 127 | 1. Using the `unit of work` pattern employed by `Session`. `UPDATE` operations for each primary key with changes are sent out in sequence. 128 | 2. Known as "ORM usage update", where you can explicitly use the `Update` construct with Session." 129 | 130 | ### Updating changes automatically 131 | 132 | ```python 133 | >>> sandy = session.execute(select(User).filter_by(name="sandy")).scalar_one() 134 | """ 135 | SELECT user_account.id, user_account.name, user_account.fullname 136 | FROM user_account 137 | WHERE user_account.name = ? 138 | [...] ('sandy',) 139 | """ 140 | ``` 141 | 142 | This 'Sandy' user object acts as a _proxy_ for a row in the database, more specifically, for the row with primary key `2` from the transaction's perspective. 143 | 144 | ```python 145 | >>> sandy 146 | User(id=2, name='sandy', fullname='Sandy Cheeks') 147 | >>> sandy.fullname = "Sandy Squirrel" # When an object's attribute is changed, the Session records this change. 148 | >>> sandy in session.dirty # Such changed objects are referred to as 'dirty' and can be checked in session.dirty. 149 | True 150 | ``` 151 | 152 | When the `Session` executes `flush`, an `UPDATE` is executed in the database, actually updating the values in the database. If a `SELECT` statement is executed afterwards, a `flush` is automatically executed, allowing you to immediately retrieve the updated name value of Sandy through `SELECT`. 153 | 154 | ```python 155 | >>> sandy_fullname = session.execute( 156 | ... select(User.fullname).where(User.id == 2) 157 | ... ).scalar_one() 158 | """ 159 | UPDATE user_account SET fullname=? WHERE user_account.id = ? 160 | [...] ('Sandy Squirrel', 2) 161 | SELECT user_account.fullname 162 | FROM user_account 163 | WHERE user_account.id = ? 164 | [...] (2,) 165 | """ 166 | >>> print(sandy_fullname) 167 | Sandy Squirrel 168 | # Using the flush, Sandy's changes are actually reflected in the database, 169 | # causing the object to lose its 'dirty' status. 170 | >>> sandy in session.dirty 171 | False 172 | ``` 173 | 174 | ### ORM usage update 175 | 176 | The last method to perform an `UPDATE` through ORM is to explicitly use 'ORM usage update'. This allows you to use a general SQL `UPDATE` statement that can affect many rows at once. 177 | 178 | 179 | ```python 180 | >>> session.execute( 181 | ... update(User). 182 | ... where(User.name == "sandy"). 183 | ... values(fullname="Sandy Squirrel Extraordinaire") 184 | ... ) 185 | """ 186 | UPDATE user_account SET fullname=? WHERE user_account.name = ? 187 | [...] ('Sandy Squirrel Extraordinaire', 'sandy') 188 | """ 189 | 190 | ``` 191 | If there are objects in the current `Session` that match the given conditions, the corresponding `update` will also be reflected in these objects. 192 | 193 | ```python 194 | >>> sandy.fullname 195 | 'Sandy Squirrel Extraordinaire' 196 | ``` 197 | 198 |
199 | 200 | ## How to Delete ORM objects 201 | 202 | You can mark individual ORM objects for deletion using the `Session.delete()` method. Once `delete` is executed, objects in that `Session` become expired. 203 | 204 | ```python 205 | >>> patrick = session.get(User, 3) 206 | """ 207 | SELECT user_account.id AS user_account_id, user_account.name AS user_account_name, 208 | user_account.fullname AS user_account_fullname 209 | FROM user_account 210 | WHERE user_account.id = ? 211 | [...] (3,) 212 | """ 213 | >>> session.delete(patrick) # Indicate that patrick will be deleted 214 | >>> session.execute( 215 | ... select(User) 216 | ... .where(User.name == "patrick") 217 | ... ).first() # Execute flush at this point 218 | """ 219 | SELECT address.id AS address_id, address.email_address AS address_email_address, 220 | address.user_id AS address_user_id 221 | FROM address 222 | WHERE ? = address.user_id 223 | [...] (3,) 224 | DELETE FROM user_account WHERE user_account.id = ? 225 | [...] (3,) 226 | SELECT user_account.id, user_account.name, user_account.fullname 227 | FROM user_account 228 | WHERE user_account.name = ? 229 | [...] ('patrick',) 230 | """ 231 | >>> squidward in session # Once expired in the Session, the object is removed from the session. 232 | False 233 | ``` 234 | 235 | Like the 'Sandy' used in the above `UPDATE`, these actions are only within the ongoing transaction and can be undone at any time unless _committed_. 236 | 237 | ### ORM usage delete 238 | 239 | Like `UPDATE`, there is also 'ORM usage delete'. 240 | 241 | ```python 242 | # This is just an example, not a necessary operation for delete. 243 | >>> squidward = session.get(User, 4) 244 | """ 245 | SELECT user_account.id AS user_account_id, user_account.name AS user_account_name, 246 | user_account.fullname AS user_account_fullname 247 | FROM user_account 248 | WHERE user_account.id = ? 249 | [...] (4,) 250 | """ 251 | 252 | >>> session.execute(delete(User).where(User.name == "squidward")) 253 | """ 254 | DELETE FROM user_account WHERE user_account.name = ? 255 | [...] ('squidward',) 256 | 257 | """ 258 | ``` 259 | 260 |
261 | 262 | ## Rolling Back 263 | 264 | `Session` has a `Session.rollback()` method to roll back the current operations. This method affects Python objects like the aforementioned `sandy`. 265 | Calling `Session.rollback()` not only rolls back the transaction but also turns all objects associated with this `Session` into `expired` status. This state change triggers a self-refresh the next time the object is accessed, a process known as _lazy loading_. 266 | 267 | ```python 268 | >>> session.rollback() 269 | ROLLBACK 270 | ``` 271 | 272 | Looking closely at `sandy`, which is in the `expired` state, you can see that no other information remains except for special SQLAlchemy-related status objects. 273 | 274 | ```python 275 | >>> sandy.__dict__ 276 | {'_sa_instance_state': } 277 | >>> sandy.fullname # Since the session is expired, accessing the object properties will trigger a new transaction. 278 | """ 279 | SELECT user_account.id AS user_account_id, user_account.name AS user_account_name, 280 | user_account.fullname AS user_account_fullname 281 | FROM user_account 282 | WHERE user_account.id = ? 283 | [...] (2,) 284 | """ 285 | 'Sandy Cheeks' 286 | >>> sandy.__dict__ # Now you can see that the database row is also filled in the sandy object. 287 | {'_sa_instance_state': , 288 | 'id': 2, 'name': 'sandy', 'fullname': 'Sandy Cheeks'} 289 | ``` 290 | 291 | For the deleted objects, you can see that they are restored in the `Session` and appear again in the database. 292 | 293 | ```python 294 | >>> patrick in session 295 | True 296 | >>> session.execute(select(User).where(User.name == 'patrick')).scalar_one() is patrick 297 | """ 298 | SELECT user_account.id, user_account.name, user_account.fullname 299 | FROM user_account 300 | WHERE user_account.name = ? 301 | [...] ('patrick',) 302 | """ 303 | True 304 | ``` 305 | 306 |
307 | 308 | ## Closing the `Session` 309 | 310 | We have handled the `Session` outside of the context structure, and in such cases, it is good practice to _explicitly_ close the `Session` as follows: 311 | 312 | ```python 313 | >>> session.close() 314 | ROLLBACK 315 | ``` 316 | 317 | Similarly, when a `Session` created through a context statement is closed within the context statement, the following actions are performed. 318 | 319 | - Cancel all ongoing transactions (e.g., rollbacks) to release all connection resources to the connection pool. 320 | - This means you don't need to explicitly call `Session.rollback()` to check if the transaction was rolled back when closing the `Session` after performing some read-only operations with it. The connection pool handles this. 321 | - Remove all objects from the `Session`. 322 | - This means that all Python objects loaded for this Session, such as `sandy`, `patrick`, and `squidward`, are now in a `detached` state. For instance, an object that was in the `expired` state is no longer associated with a database transaction to refresh data due to a `Session.commit()` call, and it does not contain the current row's state. 323 | - ```python 324 | >>> squidward.name 325 | Traceback (most recent call last): 326 | ... 327 | sqlalchemy.orm.exc.DetachedInstanceError: Instance is not bound to a Session; attribute refresh operation cannot proceed 328 | ``` 329 | - Detached objects can be reassociated with the same or a new `Session` using the `Session.add()` method, re-establishing the relationship with a specific database row. 330 | - ```python 331 | >>> session.add(squidward) # Reconnect to the session 332 | >>> squidward.name # Retrieve the information through the transaction again. 333 | """ 334 | SELECT user_account.id AS user_account_id, user_account.name AS user_account_name, user_account.fullname AS user_account_fullname 335 | FROM user_account 336 | WHERE user_account.id = ? 337 | [...] (4,) 338 | """ 339 | 'squidward' 340 | ``` 341 | 342 | > Objects in the `detached` state should ideally be avoided. When a `Session` is closed, it cleans up references to all previously connected objects. Typically, the need for `detached` objects arises in web applications when an object has just been committed and the `Session` is closed before it is rendered in a view. In this case, set the `Session.expire_on_commit` flag to `False`. 343 | -------------------------------------------------------------------------------- /src/en/tutorial/7. Working with Related Objects Using ORM.md: -------------------------------------------------------------------------------- 1 | # Working with Related Objects Using ORM 2 | 3 |
4 | 5 | In this chapter, we will cover another essential ORM concept, which is the interaction with mapped objects that reference other objects. 6 | 7 | `relationship()` defines the relationship between two mapped objects and is also known as **self-referencing**. 8 | 9 | For simplicity, we will omit `Column` mappings and other directives, and explain `relationship()` in a shortened form. 10 | 11 |
12 | 13 | ```python 14 | from sqlalchemy.orm import relationship 15 | 16 | 17 | class User(Base): 18 | __tablename__ = 'user_account' 19 | 20 | # ... Column mappings 21 | 22 | addresses = relationship("Address", back_populates="user") 23 | 24 | 25 | class Address(Base): 26 | __tablename__ = 'address' 27 | 28 | # ... Column mappings 29 | 30 | user = relationship("User", back_populates="addresses") 31 | ``` 32 | 33 |
34 | 35 | In the structure shown, the `User` object has a variable `addresses`, and the `Address` object has a variable `user`. 36 | 37 | Both are created as relationship objects, but these aren't __actual database columns__ but are set up to allow __easy access__ in the code. 38 | 39 | In other words, it facilitates easy navigation from a `User` object to an `Address` object. 40 | 41 | Additionally, the `back_populates` parameter in the `relationship` declaration allows for the reverse situation, i.e., navigating from an `Address` object to a `User` object. 42 | 43 | > In relational Database terms, it naturally sets a 1 : N relationship as an N : 1 relationship. 44 | 45 | In the next section, we will see what role the `relationship()` object's instances play and how they function. 46 | 47 |
48 | 49 | ## Using Related Objects 50 | 51 |
52 | 53 | When a new `User` object is created, the `.addresses` collection appears as a `List` object. 54 | 55 | ```python 56 | >>> u1 = User(name='pkrabs', fullname='Pearl Krabs') 57 | >>> u1.addresses 58 | [] 59 | ``` 60 | 61 | You can add an `Address` object using `list.append()`. 62 | 63 | ```python 64 | >>> a1 = Address(email_address="pear1.krabs@gmail.com") 65 | >>> u1.addresses.append(a1) 66 | 67 | # The u1.addresses collection now includes the new Address object. 68 | >>> u1.addresses 69 | [Address(id=None, email_address='pearl.krabs@gmail.com')] 70 | ``` 71 | 72 | If an `Address` object is associated with the `User.addresses` collection, another action occurs in the variable `u1`. The User.addresses and Address.user relationship is synchronized, allowing you to move: 73 | - From a `User` object to an `Address`, and 74 | - Back from an `Address` object to a `User`. 75 | 76 | ```python 77 | >>> a1.user 78 | User(id=None, name='pkrabs', fullname='Pearl Krabs') 79 | ``` 80 | 81 | This is the result of synchronization using `relationship.back_populates` between the two `relationship()` objects. 82 | 83 | The `relationship()` parameter can be complementarily assigned/list modified to another variable. Creating another `Address` object and assigning it to the `Address.user` property makes it part of the `User.addresses` collection. 84 | 85 | ```python 86 | >>> a2 = Address(email_address="pearl@aol.com", user=u1) 87 | >>> u1.addresses 88 | [Address(id=None, email_address='pearl.krabs@gmail.com'), Address(id=None, email_address='pearl@aol.com')] 89 | ``` 90 | 91 |
92 | 93 | We actually used the variable `u1` as a keyword argument for `user` as if it were a property declared in the object (`Address`). It's equivalent to assigning the property afterward. 94 | 95 | ```python 96 | # equivalent effect as a2 = Address(user=u1) 97 | >>> a2.user = u1 98 | ``` 99 | 100 |
101 | 102 | ## Cascading Objects in the `Session` 103 | 104 |
105 | 106 | We now have two related `User` and `Address` objects in a bidirectional structure in memory, but as mentioned earlier in [Inserting Rows with ORM] , these objects are in a [transient] state in the `Session` until they are associated with it. 107 | 108 | We need to see when using `Session.add()`, and applying the method to the `User` object, that the related `Address` objects are also added. 109 | 110 | ```python 111 | >>> session.add(u1) 112 | >>> u1 in session 113 | True 114 | >>> a1 in session 115 | True 116 | >>> a2 in session 117 | True 118 | ``` 119 | 120 | The three objects are now in a [pending] state, which means no `INSERT` operations have been executed yet. The three objects have not been assigned primary keys, and the `a1` and `a2` objects have a column (`user_id`) reference property. This is because the objects are not yet actually connected to a real database. 121 | 122 | ```python 123 | >>> print(u1.id) 124 | None 125 | >>> print(a1.user_id) 126 | None 127 | ``` 128 | 129 | Let's save it to the database. 130 | 131 | ```python 132 | >>> session.commit() 133 | ``` 134 | 135 | If we translate the implemented code into SQL queries, it would look like this. 136 | 137 | ```sql 138 | INSERT INTO user_account (name, fullname) VALUES (?, ?) 139 | [...] ('pkrabs', 'Pearl Krabs') 140 | INSERT INTO address (email_address, user_id) VALUES (?, ?) 141 | [...] ('pearl.krabs@gmail.com', 6) 142 | INSERT INTO address (email_address, user_id) VALUES (?, ?) 143 | [...] ('pearl@aol.com', 6) 144 | COMMIT 145 | ``` 146 | 147 | Using session, you can automate SQL `INSERT`, `UPDATE`, `DELETE` statements. 148 | 149 | Finally, executing `Session.commit()` ensures all steps are called in the correct order, and the primary key of `address.user_id` is applied in the `user_account`. 150 | 151 |
152 | 153 | ## Loading Relationships 154 | 155 |
156 | 157 | After calling `Session.commit()`, you can see the primary key created for the `u1` object. 158 | 159 | ```python 160 | >>> u1.id 161 | 6 162 | ``` 163 | 164 | > The above code is equivalent to executing the following query. 165 | 166 | ```sql 167 | BEGIN (implicit) 168 | SELECT user_account.id AS user_account_id, user_account.name AS user_account_name, 169 | user_account.fullname AS user_account_fullname 170 | FROM user_account 171 | WHERE user_account.id = ? 172 | [...] (6,) 173 | ``` 174 | 175 | You can also see that `id`s are now present in the objects linked to `u1.addresses`. 176 | 177 | To retrieve these objects, we can observe the **lazy load** approach. 178 | 179 | > lazy loading : This is a method where a SELECT statement is executed to fetch information only when someone tries to access that information. In other words, it retrieves the necessary information as needed. 180 | 181 | ```python 182 | >>> u1.addresses 183 | [Address(id=4, email_address='pearl.krabs@gmail.com'), Address(id=5, email_address='pearl@aol.com')] 184 | ``` 185 | 186 | ```sql 187 | SELECT address.id AS address_id, address.email_address AS address_email_address, 188 | address.user_id AS address_user_id 189 | FROM address 190 | WHERE ? = address.user_id 191 | [...] (6,) 192 | ``` 193 | 194 | SQLAlchemy ORM’s default for collections and related properties is **lazy loading**. This means once a collection has been *relationshipped*, as long as the data exists in memory, it remains accessible. 195 | 196 | ```python 197 | >>> u1.addresses 198 | [Address(id=4, email_address='pearl.krabs@gmail.com'), Address(id=5, email_address='pearl@aol.com')] 199 | ``` 200 | Although lazy loading can be costly without explicit steps for optimization, it is optimized at least not to perform redundant operations. 201 | 202 | You can also see the `a1` and `a2` objects in the `u1.addresses` collection. 203 | 204 | ```python 205 | >>> a1 206 | Address(id=4, email_address='pearl.krabs@gmail.com') 207 | >>> a2 208 | Address(id=5, email_address='pearl@aol.com') 209 | ``` 210 | 211 | We will provide a further introduction to the concept of `relationship` in the latter part of this section. 212 | 213 |
214 | 215 | ## Using `relationship` in Queries 216 | 217 |
218 | 219 | This section introduces several ways in which `relationship()` helps automate SQL query construction. 220 | 221 |
222 | 223 | ### JOIN tables using `relationship()` 224 | 225 | In [Specifying the FROM and JOIN Clauses] and [WHERE Clauses] sections, we used `Select.join()` and `Select.join_from()` methods to construct SQL JOINs. These methods infer the ON clause based on whether there's a `ForeignKeyConstraint` object linking the two tables or provide specific SQL Expression syntax representing the `ON` clause. 226 | 227 | `relationship()` objects can be used to set the `ON` clause for joins. 228 | A `relationship()` corresponding object can be passed as a **single argument** to `Select.join()`, serving as both the right join and the ON clause. 229 | 230 | ```python 231 | >>> print( 232 | ... select(Address.email_address). 233 | ... select_from(User). 234 | ... join(User.addresses) 235 | ... ) 236 | ``` 237 | > The above code is equivalent to executing the following query. 238 | ```sql 239 | SELECT address.email_address 240 | FROM user_account JOIN address ON user_account.id = address.user_id 241 | ``` 242 | 243 | If `relationship()` is not specified in `Select.join()` or `Select.join_from()`, **no ON clause is used**. This means it functions due to the `ForeignKeyConstraint` between the two mapped table objects, not because of the `relationship()` object of `User` and `Address`. 244 | 245 | ```python 246 | >>> print( 247 | ... select(Address.email_address). 248 | ... join_from(User, Address) 249 | ... ) 250 | ``` 251 | > The above code is equivalent to executing the following query. 252 | ```sql 253 | SELECT address.email_address 254 | FROM user_account JOIN address ON user_account.id = address.user_id 255 | ``` 256 | 257 |
258 | 259 | ### Joining Using Aliases(`aliased`) 260 | 261 | When configuring SQL JOINs using `relationship()`, it's suitable to use [PropComparator.of_type()] with `aliased()` cases. However, `relationship()` is used to configure the same joins as described in [`ORM Entity Aliases`]. 262 | 263 | You can directly use `aliased()` in a join with `relationship()`. 264 | 265 | ```python 266 | >>> from sqlalchemy.orm import aliased 267 | >>> address_alias_1 = aliased(Address) 268 | >>> address_alias_2 = aliased(Address) 269 | >>> print( 270 | ... select(User). 271 | ... join_from(User, address_alias_1). 272 | ... where(address_alias_1.email_address == 'patrick@aol.com'). 273 | ... join_from(User, address_alias_2). 274 | ... where(address_alias_2.email_address == 'patrick@gmail.com') 275 | ... ) 276 | ``` 277 | > The above code is equivalent to executing the following query. 278 | ```sql 279 | SELECT user_account.id, user_account.name, user_account.fullname 280 | FROM user_account 281 | JOIN address AS address_1 ON user_account.id = address_1.user_id 282 | JOIN address AS address_2 ON user_account.id = address_2.user_id 283 | WHERE address_1.email_address = :email_address_1 284 | AND address_2.email_address = :email_address_2 285 | ``` 286 | 287 | You can use join clause in `aliased()` object using `relationship()`. 288 | 289 | ```python 290 | >>> user_alias_1 = aliased(User) 291 | >>> print( 292 | ... select(user_alias_1.name). 293 | ... join(user_alias_1.addresses) 294 | ... ) 295 | ``` 296 | > The above code is equivalent to executing the following query. 297 | ```sql 298 | SELECT user_account_1.name 299 | FROM user_account AS user_account_1 300 | JOIN address ON user_account_1.id = address.user_id 301 | ``` 302 | 303 |
304 | 305 | ### Expanding ON Conditions 306 | 307 | You can add conditions to the ON clause created by `relation()`. This feature is useful not only for quickly limiting the scope of a specific join for a related path but also for use cases like loader strategy configuration introduced in the last section. 308 | [`PropComparator.and_()`] method allows a series of SQL expressions to be positionally combined in the JOIN's `ON` clause via `AND`. 309 | For example, to limit the ON criteria to specific email addresses using `User` and `Address`, you would do this. 310 | 311 | ```python 312 | >>> stmt = ( 313 | ... select(User.fullname). 314 | ... join(User.addresses.and_(Address.email_address == 'pearl.krabs@gmail.com')) 315 | ... ) 316 | 317 | >>> session.execute(stmt).all() 318 | [('Pearl Krabs',)] 319 | ``` 320 | > The above code is equivalent to executing the following query. 321 | ```sql 322 | SELECT user_account.fullname 323 | FROM user_account 324 | JOIN address ON user_account.id = address.user_id AND address.email_address = ? 325 | [...] ('pearl.krabs@gmail.com',) 326 | ``` 327 | 328 |
329 | 330 | ### EXISTS `has()` , `and()` 331 | 332 | In the [EXISTS Subqueries] section, the SQL EXISTS keyword was introduced along with the [Scalar Subqueries, Correlated Queries] section. 333 | `relationship()` provides some help in commonly creating subqueries for relationships. 334 | 335 |
336 | 337 | For a 1:N (one-to-many) relationship like `User.addresses`, you can use `PropComparator.any()` to create a subquery for the address table rejoining the `user_account` table. This method allows optional WHERE criteria to limit the rows matching the subquery. 338 | 339 | ```python 340 | >>> stmt = ( 341 | ... select(User.fullname). 342 | ... where(User.addresses.any(Address.email_address == 'pearl.krabs@gmail.com')) 343 | ... ) 344 | 345 | >>> session.execute(stmt).all() 346 | [('Pearl Krabs',)] 347 | ``` 348 | > The above code is equivalent to executing the following query. 349 | ```sql 350 | SELECT user_account.fullname 351 | FROM user_account 352 | WHERE EXISTS (SELECT 1 353 | FROM address 354 | WHERE user_account.id = address.user_id AND address.email_address = ?) 355 | [...] ('pearl.krabs@gmail.com',) 356 | ``` 357 | 358 | Conversely, to find objects without related data, use `~User.addresses.any()` to search for `User` objects. 359 | 360 | ```python 361 | >>> stmt = ( 362 | ... select(User.fullname). 363 | ... where(~User.addresses.any()) 364 | ... ) 365 | 366 | >>> session.execute(stmt).all() 367 | [('Patrick McStar',), ('Squidward Tentacles',), ('Eugene H. Krabs',)] 368 | ``` 369 | > The above code is equivalent to executing the following query. 370 | ```sql 371 | SELECT user_account.fullname 372 | FROM user_account 373 | WHERE NOT (EXISTS (SELECT 1 374 | FROM address 375 | WHERE user_account.id = address.user_id)) 376 | [...] () 377 | 378 | ``` 379 | 380 | `PropComparator.has()` works similarly to `PropComparator.any()` but is used for N:1 (Many-to-one) relationships. 381 | For instance, to find all `Address` objects belonging to "pearl", you would use this method. 382 | 383 | ```python 384 | >>> stmt = ( 385 | ... select(Address.email_address). 386 | ... where(Address.user.has(User.name=="pkrabs")) 387 | ... ) 388 | 389 | >>> session.execute(stmt).all() 390 | [('pearl.krabs@gmail.com',), ('pearl@aol.com',)] 391 | ``` 392 | > The above code is equivalent to executing the following query. 393 | 394 | ```sql 395 | SELECT address.email_address 396 | FROM address 397 | WHERE EXISTS (SELECT 1 398 | FROM user_account 399 | WHERE user_account.id = address.user_id AND user_account.name = ?) 400 | [...] ('pkrabs',) 401 | ``` 402 | 403 |
404 | 405 | ### Relationship Operators 406 | 407 | Several types of SQL creation helpers come with `relationship()`: 408 | 409 | - N : 1 (Many-to-one) comparison 410 | You can select rows where the foreign key of the target entity matches the primary key value of a specified object instance in an N:1 relationship. 411 | 412 | ```python 413 | >>> print(select(Address).where(Address.user == u1)) 414 | ``` 415 | > The above code is equivalent to executing the following query. 416 | ```sql 417 | SELECT address.id, address.email_address, address.user_id 418 | FROM address 419 | WHERE :param_1 = address.user_id 420 | ``` 421 | 422 | - NOT N : 1 (Many-to-one) comparison 423 | You can use the not equal (!=) operator. 424 | 425 | ```python 426 | >>> print(select(Address).where(Address.user != u1)) 427 | ``` 428 | > The above code is equivalent to executing the following query. 429 | ```sql 430 | SELECT address.id, address.email_address, address.user_id 431 | FROM address 432 | WHERE address.user_id != :user_id_1 OR address.user_id IS NULL 433 | ``` 434 | 435 | - You can check if an object is included in a 1:N (one-to-many) collection. 436 | ```python 437 | >>> print(select(User).where(User.addresses.contains(a1))) 438 | ``` 439 | > The above code is equivalent to executing the following query. 440 | ```sql 441 | SELECT user_account.id, user_account.name, user_account.fullname 442 | FROM user_account 443 | WHERE user_account.id = :param_1 444 | ``` 445 | 446 | - You can check if an object in a 1:N relationship is part of a specific parent item. `with_parent()` creates a comparison that returns rows referencing the given parent item, equivalent to using the `==` operator." 447 | 448 | 449 | ```python 450 | >>> from sqlalchemy.orm import with_parent 451 | >>> print(select(Address).where(with_parent(u1, User.addresses))) 452 | ``` 453 | > The above code is equivalent to executing the following query. 454 | ```sql 455 | SELECT address.id, address.email_address, address.user_id 456 | FROM address 457 | WHERE :param_1 = address.user_id 458 | ``` 459 | 460 |
461 | 462 | ## Types of Relationship Loading 463 | 464 |
465 | 466 | In the [Loading Relationships](#loading-relationships) section, we introduced the concept that when working with mapped object instances and accessing mapped attributes using `relationship()`, objects that should be in this collection are loaded, and if the collection is not filled, _lazy load_ occurs. 467 | 468 | Lazy loading is one of the most famous ORM patterns and also the most controversial. If dozens of ORM objects in memory each refer to a few unloaded properties, the routine manipulation of objects can implicitly release many problems ([`N+1 Problem`]), which can accumulate. Such implicit queries may not work at all when attempting database transformations that are no longer viable or when using alternative concurrency patterns like asynchronous. 469 | 470 | > What is a [`N + 1 Problem`]? 471 | > It's a problem where you fetch N records with one query, but to get the desired data, you end up performing a secondary query for each of these N records. 472 | 473 | Lazy loading is a very popular and useful pattern when it is compatible with the concurrency approach in use and does not cause other problems. For this reason, SQLAlchemy's ORM focuses on features that allow you to permit and optimize these load behaviors. 474 | 475 | Above all, the first step to effectively using ORM's lazy loading is to **test the Application and check the SQL**. 476 | If inappropriate loads occur for objects detached from the `Session`, the use of **[`Types of Relationship Loading`](#types-of-relationship-loading)** should be reviewed. 477 | 478 | You can mark objects to be associated with a SELECT statement using the `Select.options()` method. 479 | 480 | ```python 481 | for user_obj in session.execute( 482 | select(User).options(selectinload(User.addresses)) 483 | ).scalars(): 484 | user_obj.addresses # access addresses collection already loaded 485 | ``` 486 | 487 | You can also configure it as a default for `relationship()` using `relationship.lazy`. 488 | 489 | ```sql 490 | from sqlalchemy.orm import relationship 491 | class User(Base): 492 | __tablename__ = 'user_account' 493 | 494 | addresses = relationship("Address", back_populates="user", lazy="selectin") 495 | ``` 496 | 497 | > 498 | > cf. **Two Techniques of Relationship Loading** 499 | > 500 | > - [`Configuring Loader Strategies at Mapping Time`] 501 | > - Details about `relationship()` configuration 502 | > - [`Relationship Loading with Loader Options`] 503 | > - Details about the loader 504 | 505 |
506 | 507 | ### Select IN loading Method 508 | 509 | The most useful loading option in recent SQLAlchemy versions is `selectinload()`. This option solves the most common form of the "N+1 Problem" problem, which is an issue with sets of objects referencing related collections. It typically uses a SELECT form that can be sent out for the related table without introducing JOINs or subqueries and only queries for parent objects whose collections are not loaded. 510 | 511 | The following example shows the Address objects related to a `User` object being loaded with `selectinload()`. During the `Session.execute()` call, two SELECT statements are generated in the database, with the second fetching the related `Address` objects. 512 | 513 | ```sql 514 | >>> from sqlalchemy.orm import selectinload 515 | >>> stmt = ( 516 | ... select(User).options(selectinload(User.addresses)).order_by(User.id) 517 | ... ) 518 | >>> for row in session.execute(stmt): 519 | ... print(f"{row.User.name} ({', '.join(a.email_address for a in row.User.addresses)})") 520 | spongebob (spongebob@sqlalchemy.org) 521 | sandy (sandy@sqlalchemy.org, sandy@squirrelpower.org) 522 | patrick () 523 | squidward () 524 | ehkrabs () 525 | pkrabs (pearl.krabs@gmail.com, pearl@aol.com) 526 | ``` 527 | > The above code is equivalent to executing the following query. 528 | ```sql 529 | SELECT user_account.id, user_account.name, user_account.fullname 530 | FROM user_account ORDER BY user_account.id 531 | [...] () 532 | SELECT address.user_id AS address_user_id, address.id AS address_id, 533 | address.email_address AS address_email_address 534 | FROM address 535 | WHERE address.user_id IN (?, ?, ?, ?, ?, ?) 536 | [...] (1, 2, 3, 4, 5, 6) 537 | ``` 538 | 539 |
540 | 541 | ### Joined Loading Method 542 | 543 | _Joined Loading_, the oldest in SQLAlchemy, is a type of eager loading, also known as `joined eager loading`. It is best suited for loading objects in "N:1 relationships", as it performs a SELECT JOIN of the tables specified in `relationship()`, fetching all table data at once. 544 | 545 | For example, where an `Address` object has a connected user, an INNER JOIN can be used rather than an OUTER JOIN. 546 | 547 | ```python 548 | >>> from sqlalchemy.orm import joinedload 549 | >>> stmt = ( 550 | ... select(Address).options(joinedload(Address.user, innerjoin=True)).order_by(Address.id) 551 | ... ) 552 | >>> for row in session.execute(stmt): 553 | ... print(f"{row.Address.email_address} {row.Address.user.name}") 554 | 555 | spongebob@sqlalchemy.org spongebob 556 | sandy@sqlalchemy.org sandy 557 | sandy@squirrelpower.org sandy 558 | pearl.krabs@gmail.com pkrabs 559 | pearl@aol.com pkrabs 560 | ``` 561 | > The above code is equivalent to executing the following query. 562 | ```sql 563 | SELECT address.id, address.email_address, address.user_id, user_account_1.id AS id_1, 564 | user_account_1.name, user_account_1.fullname 565 | FROM address 566 | JOIN user_account AS user_account_1 ON user_account_1.id = address.user_id 567 | ORDER BY address.id 568 | [...] () 569 | ``` 570 | 571 | `joinedload()` is also used for 1: N collections but should be evaluated case-by-case compared to other options like `selectinload()` due to its nested collections and larger collections. 572 | 573 | It's important to note that the WHERE and ORDER BY criteria of the SELECT query **do not target the table rendered by `joinload()`**. In the SQL query above, you can see an **anonymous alias** applied to the `user_account` table, which cannot directly address. This concept is further explained in the [Zen of joined Eager Loading] section. 574 | 575 | The ON clause by `joinedload()` can be directly influenced using the method described previously in [`Expanding ON Conditions`](#expanding-on-conditions). 576 | 577 | > cf. 578 | > 579 | > In general cases, "N+1 problem" is much less prevalent, so many-to-one eager loading is often unnecessary. 580 | > 581 | > When many objects all reference the same related object (e.g., many `Address` objects referencing the same `User`), a single SQL for the `User` object is emitted using ordinary lazy loading. 582 | > 583 | > The lazy loading routine queries the related object by the current primary key without emitting SQL if possible. 584 | 585 |
586 | 587 | ### Explicit Join + Eager Load Method 588 | 589 | A common use case uses the `contains_eager()` option, which is very similar to `joinedload()` except it assumes you have set up the JOIN directly and instead marks additional columns in the COLUMNS clause that should be loaded into each object's related properties. 590 | 591 | ```python 592 | >>> from sqlalchemy.orm import contains_eager 593 | 594 | >>> stmt = ( 595 | ... select(Address). 596 | ... join(Address.user). 597 | ... where(User.name == 'pkrabs'). 598 | ... options(contains_eager(Address.user)).order_by(Address.id) 599 | ... ) 600 | 601 | >>> for row in session.execute(stmt): 602 | ... print(f"{row.Address.email_address} {row.Address.user.name}") 603 | 604 | pearl.krabs@gmail.com pkrabs 605 | pearl@aol.com pkrabs 606 | ``` 607 | > The above code is equivalent to executing the following query. 608 | ```sql 609 | SELECT user_account.id, user_account.name, user_account.fullname, 610 | address.id AS id_1, address.email_address, address.user_id 611 | FROM address JOIN user_account ON user_account.id = address.user_id 612 | WHERE user_account.name = ? ORDER BY address.id 613 | [...] ('pkrabs',) 614 | ``` 615 | 616 | For instance, we filtered `user_account.name` and loaded it into the returned `Address.user` property. A separate application of `joinedload()` would have unnecessarily created a twice-joined SQL query. 617 | 618 | ```python 619 | >>> stmt = ( 620 | ... select(Address). 621 | ... join(Address.user). 622 | ... where(User.name == 'pkrabs'). 623 | ... options(joinedload(Address.user)).order_by(Address.id) 624 | ... ) 625 | >>> print(stmt) # SELECT has a JOIN and LEFT OUTER JOIN unnecessarily 626 | ``` 627 | > The above code is equivalent to executing the following query. 628 | ```sql 629 | SELECT address.id, address.email_address, address.user_id, 630 | user_account_1.id AS id_1, user_account_1.name, user_account_1.fullname 631 | FROM address JOIN user_account ON user_account.id = address.user_id 632 | LEFT OUTER JOIN user_account AS user_account_1 ON user_account_1.id = address.user_id 633 | WHERE user_account.name = :name_1 ORDER BY address.id 634 | ``` 635 | 636 |
637 | 638 | > cf. 639 | > 640 | > **Two Techniques of Relationship Loading** 641 | > [`Zen of joined Eager Loading`] 642 | > - Details about this loading method 643 | > [`Routing Explicit Joins/Statements into Eagerly Loaded Collections`] 644 | > - using `contains_eager()` 645 | 646 |
647 | 648 | ### Setting Loader Paths 649 | 650 | The `PropComparator.and_()` method is actually generally usable for most loader options. 651 | 652 | For example, if you want to reload usernames and email addresses from the `sqlalchemy.org` domain, you can limit the conditions with `PropComparator.and_()` applied to the arguments passed to `selectinload()`. 653 | 654 | ```python 655 | >>> from sqlalchemy.orm import selectinload 656 | >>> stmt = ( 657 | ... select(User). 658 | ... options( 659 | ... selectinload( 660 | ... User.addresses.and_( 661 | ... ~Address.email_address.endswith("sqlalchemy.org") 662 | ... ) 663 | ... ) 664 | ... ). 665 | ... order_by(User.id). 666 | ... execution_options(populate_existing=True) 667 | ... ) 668 | 669 | >>> for row in session.execute(stmt): 670 | ... print(f"{row.User.name} ({', '.join(a.email_address for a in row.User.addresses)})") 671 | 672 | spongebob () 673 | sandy (sandy@squirrelpower.org) 674 | patrick () 675 | squidward () 676 | ehkrabs () 677 | pkrabs (pearl.krabs@gmail.com, pearl@aol.com) 678 | ``` 679 | > The above code is equivalent to executing the following query. 680 | ```sql 681 | SELECT user_account.id, user_account.name, user_account.fullname 682 | FROM user_account ORDER BY user_account.id 683 | [...] () 684 | SELECT address.user_id AS address_user_id, address.id AS address_id, 685 | address.email_address AS address_email_address 686 | FROM address 687 | WHERE address.user_id IN (?, ?, ?, ?, ?, ?) 688 | AND (address.email_address NOT LIKE '%' || ?) 689 | [...] (1, 2, 3, 4, 5, 6, 'sqlalchemy.org') 690 | ``` 691 | 692 | It's crucial to note the addition of the `.execution_options(populate_existing=True)` **option** above. When fetching rows, this option indicates that loader options must replace the existing collections' contents in already loaded objects. 693 | 694 | Since we are iterating with a `Session` object, the objects being loaded here are the same Python instances as those initially maintained at the start of this tutorial's ORM section. 695 | 696 |
697 | 698 | ### Raise Loading Method 699 | 700 | The `raiseload()` option is commonly used to completely block the occurrence of the "N+1 problem" by instead causing errors rather than slow loading. 701 | 702 | There are two variants: blocking all "load" operations that include works that need SQL (_lazy load_) and those that only reference the current `Session` (`raiseload.sql_only` **option**). 703 | 704 | 705 | ```python 706 | class User(Base): 707 | __tablename__ = 'user_account' 708 | 709 | # ... Column mappings 710 | 711 | addresses = relationship("Address", back_populates="user", lazy="raise_on_sql") 712 | 713 | 714 | class Address(Base): 715 | __tablename__ = 'address' 716 | 717 | # ... Column mappings 718 | 719 | user = relationship("User", back_populates="addresses", lazy="raise_on_sql") 720 | ``` 721 | 722 | Using such mappings blocks the application from 'lazy loading', requiring you to specify loader strategies for specific queries. 723 | 724 | ```python 725 | u1 = s.execute(select(User)).scalars().first() 726 | u1.addresses 727 | sqlalchemy.exc.InvalidRequestError: 'User.addresses' is not available due to lazy='raise_on_sql' 728 | ``` 729 | 730 | The exception indicates that the collection must be loaded first. 731 | 732 | ```python 733 | u1 = s.execute(select(User).options(selectinload(User.addresses))).scalars().first() 734 | ``` 735 | 736 | The `lazy="raise_on_sql"` option is also wisely attempted for N:1 relationships. 737 | 738 | Above, although the `Address.user` property was not loaded into `Address`, "raiseload" does not cause an error because the corresponding `User` object is in the same `Session`. 739 | 740 | > cf. 741 | > 742 | > [Preventing unwanted lazy loading with `raiseload`] 743 | > [Preventing lazy loading in `relationship`] 744 | 745 | 746 | 747 | [Inserting Rows with ORM]: (https://soogoonsoogoonpythonists.github.io/sqlalchemy-for-pythonist/en/tutorial/6.%20Manipulating%20Data%20Using%20ORM.html#inserting-rows-with-orm) 748 | [transient]: (https://docs.sqlalchemy.org/en/20/glossary.html#term-transient) 749 | [pending]: (https://docs.sqlalchemy.org/en/20/glossary.html#term-pending) 750 | 751 | [Specifying the FROM and JOIN Clauses]: (https://soogoonsoogoonpythonists.github.io/sqlalchemy-for-pythonist/tutorial/5.%20%EB%8D%B0%EC%9D%B4%ED%84%B0%20%ED%95%B8%EB%93%A4%EB%A7%81%20-%20Core,%20ORM%EC%9C%BC%EB%A1%9C%20%ED%96%89%20%EC%A1%B0%ED%9A%8C%ED%95%98%EA%B8%B0.html#from%E1%84%8C%E1%85%A5%E1%86%AF%E1%84%80%E1%85%AA-join-%E1%84%86%E1%85%A7%E1%86%BC%E1%84%89%E1%85%B5%E1%84%92%E1%85%A1%E1%84%80%E1%85%B5) 752 | 753 | [WHERE Clauses]: (https://soogoonsoogoonpythonists.github.io/sqlalchemy-for-pythonist/tutorial/5.%20%EB%8D%B0%EC%9D%B4%ED%84%B0%20%ED%95%B8%EB%93%A4%EB%A7%81%20-%20Core,%20ORM%EC%9C%BC%EB%A1%9C%20%ED%96%89%20%EC%A1%B0%ED%9A%8C%ED%95%98%EA%B8%B0.html#where%E1%84%8C%E1%85%A5%E1%86%AF) 754 | 755 | 756 | [`PropComparator.and_()`]: (https://docs.sqlalchemy.org/en/14/orm/internals.html#sqlalchemy.orm.PropComparator.and_) 757 | 758 | [EXISTS subqueries]: (https://soogoonsoogoonpythonists.github.io/sqlalchemy-for-pythonist/tutorial/5.%20%EB%8D%B0%EC%9D%B4%ED%84%B0%20%ED%95%B8%EB%93%A4%EB%A7%81%20-%20Core,%20ORM%EC%9C%BC%EB%A1%9C%20%ED%96%89%20%EC%A1%B0%ED%9A%8C%ED%95%98%EA%B8%B0.html#exists-%E1%84%89%E1%85%A5%E1%84%87%E1%85%B3%E1%84%8F%E1%85%AF%E1%84%85%E1%85%B5%E1%84%83%E1%85%B3%E1%86%AF) 759 | 760 | [Scalar Subqueries, Correlated Queries]: (https://soogoonsoogoonpythonists.github.io/sqlalchemy-for-pythonist/tutorial/5.%20%EB%8D%B0%EC%9D%B4%ED%84%B0%20%ED%95%B8%EB%93%A4%EB%A7%81%20-%20Core,%20ORM%EC%9C%BC%EB%A1%9C%20%ED%96%89%20%EC%A1%B0%ED%9A%8C%ED%95%98%EA%B8%B0.html#%E1%84%89%E1%85%B3%E1%84%8F%E1%85%A1%E1%86%AF%E1%84%85%E1%85%A1-%E1%84%89%E1%85%A5%E1%84%87%E1%85%B3-%E1%84%8F%E1%85%AF%E1%84%85%E1%85%B5-%E1%84%89%E1%85%A1%E1%86%BC%E1%84%92%E1%85%A9%E1%84%8B%E1%85%A7%E1%86%AB%E1%84%80%E1%85%AA%E1%86%AB-%E1%84%8F%E1%85%AF%E1%84%85%E1%85%B5) 761 | 762 | [`N + 1 Problem`]: (https://blog.naver.com/yysdntjq/222405755893) 763 | 764 | [`N+1 Problem`]: (https://docs.sqlalchemy.org/en/20/glossary.html#term-N-plus-one-problem) 765 | 766 | [`Zen of joined Eager Loading`]: (https://docs.sqlalchemy.org/en/20/orm/loading_relationships.html#zen-of-eager-loading) 767 | 768 | [`Routing Explicit Joins/Statements into Eagerly Loaded Collections`]: (https://docs.sqlalchemy.org/en/20/orm/loading_relationships.html#contains-eager) 769 | 770 | [`Configuring Loader Strategies at Mapping Time`]: (https://docs.sqlalchemy.org/en/20/orm/loading_relationships.html#relationship-lazy-option) 771 | 772 | [`Relationship Loading with Loader Options`]: (https://docs.sqlalchemy.org/en/20/orm/loading_relationships.html#relationship-loader-options) 773 | 774 | [Preventing unwanted lazy loading with `raiseload`]: (https://docs.sqlalchemy.org/en/20/orm/loading_relationships.html#prevent-lazy-with-raiseload) 775 | 776 | [Preventing lazy loading in `relationship`]: (https://docs.sqlalchemy.org/en/20/orm/loading_relationships.html) 777 | -------------------------------------------------------------------------------- /src/en/tutorial/README.md: -------------------------------------------------------------------------------- 1 | # Tutorial 2 | 3 | This document is a translated and organized version of the [SQLAlchemy 1.4/2.0 Tutorial](https://docs.sqlalchemy.org/en/14/tutorial/). 4 | 5 | The original official documentation is challenging to navigate and contains an overwhelming amount of information. Furthermore, it can be quite difficult for beginners to understand. 6 | With this in mind, we thought of translating and organizing the SQLAlchemy Tutorial documents for easier comprehension and accessibility. The idea originated from [this post](https://github.com/SoogoonSoogoonPythonists/sqlalchemy-for-pythonist/issues/2), where people gathered to work together on this project. 7 | 8 | Over a little more than a month, we took turns each week to work on a chapter of the official Tutorial document. 9 | We reviewed each other's work, refining the articles and asking questions about parts we didn't understand. 10 | These documents are the results of such efforts and also represent the traces of our study. 11 | 12 | There are still many imperfections, and there might be errors. 13 | If you find such issues, please feel free to contribute. Your participation is always welcome. 14 | 15 | The following individuals have contributed to this project: 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/en/tutorial/config.js: -------------------------------------------------------------------------------- 1 | const { description } = require('../../package') 2 | 3 | module.exports = { 4 | base: "/sqlalchemy-for-pythonist/", 5 | 6 | // Path 7 | locales: { 8 | '/': { 9 | lang: 'ko', 10 | title: '파이썬 개발자를 위한 SQLAlchemy', 11 | description: description, 12 | }, 13 | '/en/': { 14 | lang: 'en-US', 15 | title: 'SQLAlchemy for Python Developers', 16 | description: 'This is a document that simplifies SQLAlchemy for easy understanding.', 17 | } 18 | }, 19 | 20 | /** 21 | * Extra tags to be injected to the page HTML `` 22 | * 23 | * ref:https://v1.vuepress.vuejs.org/config/#head 24 | */ 25 | head: [ 26 | ['meta', { name: 'theme-color', content: '#3eaf7c' }], 27 | ['meta', { name: 'apple-mobile-web-app-capable', content: 'yes' }], 28 | ['meta', { name: 'apple-mobile-web-app-status-bar-style', content: 'black' }], 29 | ['meta', { name: 'google-site-verification', content: 'wjX_mSoZBgO9SZMvjr96yOjo6n3_7pS8xNdmzDl1ESw' }], 30 | [ 31 | "script", 32 | { 33 | async: true, 34 | src: "https://www.googletagmanager.com/gtag/js?id=G-SNPCYHY4R2", 35 | }, 36 | ], 37 | ["script", {}, ["window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date()); gtag('config', 'G-SNPCYHY4R2');"]], 38 | ], 39 | 40 | /** 41 | * Theme configuration, here is the default theme configuration for VuePress. 42 | * 43 | * ref:https://v1.vuepress.vuejs.org/theme/default-theme-config.html 44 | */ 45 | themeConfig: { 46 | locales: { 47 | '/': { 48 | repo: '', 49 | editLinks: true, 50 | docsDir: '', 51 | editLinkText: '', 52 | lastUpdated: true, 53 | smoothScroll: true, 54 | nav: [ 55 | { 56 | text: 'GitHub', 57 | link: 'https://github.com/SoogoonSoogoonPythonists/sqlalchemy-for-pythonist' 58 | }, 59 | ], 60 | sidebar: { 61 | '/tutorial/': [ 62 | { 63 | title: 'Tutorial', 64 | path: '/tutorial/', 65 | collapsable: false, 66 | children: [ 67 | '1. 튜토리얼 개요', 68 | '2. 연결 설정하기', 69 | '3. 트랜잭션과 쿼리 실행하기', 70 | '4. 데이터베이스 메타데이터로 작업하기', 71 | '5.1. Core와 ORM 방식으로 행 조회하기', 72 | '5.2. Core 방식으로 행 삽입하기', 73 | '5.3. Core 방식으로 행 수정 및 삭제하기', 74 | '6. ORM 방식으로 데이터 조작하기', 75 | '7. ORM 방식으로 관련 개체 작업하기', 76 | ] 77 | }, 78 | ] 79 | } 80 | }, 81 | '/en/': { 82 | repo: '', 83 | editLinks: true, 84 | docsDir: '/en.', 85 | editLinkText: '/en/', 86 | lastUpdated: true, 87 | smoothScroll: true, 88 | nav: [ 89 | { 90 | text: 'GitHub', 91 | link: 'https://github.com/SoogoonSoogoonPythonists/sqlalchemy-for-pythonist' 92 | }, 93 | ], 94 | sidebar: { 95 | '/en/tutorial/': [ 96 | { 97 | title: 'Tutorial', 98 | path: '/en/tutorial/', 99 | collapsable: false, 100 | children: [ 101 | '1. tutorial', 102 | '2. connection setting', 103 | ] 104 | }, 105 | ] 106 | } 107 | }, 108 | }, 109 | }, 110 | 111 | /** 112 | * Apply plugins,ref:https://v1.vuepress.vuejs.org/zh/plugin/ 113 | */ 114 | plugins: [ 115 | '@vuepress/plugin-back-to-top', 116 | '@vuepress/plugin-medium-zoom', 117 | ["sitemap", { hostname: "https://soogoonsoogoonpythonists.github.io/sqlalchemy-for-pythonist/" }], 118 | ["@vuepress/last-updated"], 119 | ], 120 | } 121 | -------------------------------------------------------------------------------- /src/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | home: true 3 | heroImage: https://media.vlpt.us/images/zamonia500/post/6dd8b08b-a089-49db-a2f1-921ad6a9649e/connect-a-flask-app-to-a-mysql-database-with-sqlalchemy-and-pymysql.jpg 4 | tagline: SQLAlchemy를 쉽게 정리해놓은 문서입니다. 5 | actionText: Tutorial → 6 | actionLink: /tutorial/ 7 | footer: Made by soogoonsoogoon pythonists ❤️ 8 | --- 9 | -------------------------------------------------------------------------------- /src/tutorial/1. 튜토리얼 개요.md: -------------------------------------------------------------------------------- 1 | # 튜토리얼 개요 2 | 3 |
4 | 5 | ## 개요 6 | 7 | SQLAlchemy는 Python에서 데이터베이스와의 연결 및 ORM 등을 활용할 수 있도록 해주는 라이브러리 입니다. 8 | 가령 특정 쿼리를 코드에서 실행할 수 있고, ORM 객체를 통해 데이터베이스에서의 일련의 작업들을 수행할 수 있습니다. 9 | 10 |
11 | 12 | ## 설치 13 | 14 | SQLAlchemy는 다음처럼 설치할 수 있습니다. 15 | 16 | ```bash 17 | $ pip install sqlalchemy 18 | ``` 19 | 20 | 사용하고 있는 버전은 다음과 같습니다. 21 | 22 | ```python 23 | >>> import sqlalchemy 24 | >>> sqlalchemy.__version__ 25 | 1.4.20 26 | ``` 27 | 28 |
29 | 30 | ## 제공되는 것 31 | 32 | SQLAlchemy는 다음처럼 2가지로 제공됩니다. 33 | 34 | - **Core** 35 | - 데이터베이스 도구 키트로, SQLAlchemy의 기본 아키텍처입니다. 36 | - 데이터베이스에 대한 연결을 관리하고, 데이터베이스 쿼리 및 결과와 상호 작용하고, SQL 문을 프로그래밍 방식으로 구성하기위한 도구를 제공합니다. 37 | - **ORM** 38 | - Core를 기반으로 구축되어 선택적 **ORM** 기능을 제공 합니다. 39 | 40 | 기본적으로 Core에 대해 먼저 이해한 후, ORM을 사용하는게 좋습니다. 41 | 튜토리얼 역시 Core부터 설명합니다. 42 | -------------------------------------------------------------------------------- /src/tutorial/2. 연결 설정하기.md: -------------------------------------------------------------------------------- 1 | # 연결 설정하기 2 | 3 |
4 | 5 | ## 데이터베이스와 연결하기 6 | 7 | 비교적 가벼운 데이터베이스인 SQLite에 연결하는 작업을 해봅시다. 8 | 다음처럼 해볼 수 있습니다. 9 | 10 | ```python 11 | >>> from sqlalchemy import create_engine 12 | >>> engine = create_engine("sqlite+pysqlite:///:memory:", echo=True, future=True) 13 | ``` 14 | 15 | - `sqlalchemy.create_engine` 함수를 이용하여 데이터베이스와 연결을 맺는 **'엔진'** 을 만듭니다. 16 | - 첫 번째 인자로 **`문자열 URL`** 을 넘깁니다. 17 | - 일반적으로 `문자열 URL` 은 `dialect+driver://username:password@host:port/database` 의 형태로 구성됩니다. 18 | - `driver` 값을 주지 않으면 `sqlalchemy` 의 기본 설정 값이 들어가게 됩니다. 19 | - 여기서는 `sqlite+pysqlite:///test.db` 가 `문자열 URL` 입니다. 20 | - `sqlite` 의 경우 `sqlite:///` 의 포맷을 따릅니다. 21 | - 문자열 `URL` 인 `sqlite:///test.db` 에서 다음 정보들을 알 수 있습니다. 22 | - **어떤 데이터베이스**를 사용할 것인지 (`dialect` 라고 하며, 이 경우 `sqlite` 입니다) 23 | - **어떤 데이터베이스 API** (DB와 상호작용하는 드라이버) 를 사용할 것인지 (이 경우 `pysqlite` 입니다) 24 | - 데이터베이스를 **어떻게 찾을지** (이 경우 `sqlite` 에서 제공하는 인메모리를 사용합니다.) 25 | - `echo` 파라미터의 값을 `True` 를 주면 실행되는 모든 SQL을 출력해줍니다. 26 | 27 | 엔진을 만들었지만, 아직 실제로 연결을 시도한 것은 아닙니다. 실제 연결은 데이터베이스에 대해 작업을 수행하라는 요청을 처음받을 때만 발생합니다. -------------------------------------------------------------------------------- /src/tutorial/3. 트랜잭션과 쿼리 실행하기.md: -------------------------------------------------------------------------------- 1 | # 트랜잭션과 쿼리 실행하기 2 | 3 |
4 | 5 | ## 연결 얻기 6 | 7 | 다음처럼 데이터베이스에 연결하여 쿼리를 실행할 수 있습니다. 8 | 9 | ```python 10 | >>> from sqlalchemy import text 11 | 12 | >>> with engine.connect() as conn: 13 | ... result = conn.execute(text("select 'hello world'")) 14 | ... print(result.all()) 15 | 16 | [('hello world',)] 17 | ``` 18 | 19 | - `engine.connect()` 으로 [`Connection`](https://docs.sqlalchemy.org/en/14/core/future.html#sqlalchemy.future.Connection) 객체를 얻어 `conn` 에 담습니다. 20 | - 이 `Connection` 객체를 통해 데이터베이스와 상호작용할 수 있습니다. 21 | - `with ...` 구문은 하나의 트랜잭션 단위가 됩니다. 22 | - **트랜잭션은 자동으로 커밋되지 않습니다.** 23 | - `Connection.commit()` 을 호출해야 커밋됩니다. 24 | 25 |
26 | 27 | ## 변경 사항 커밋하기 28 | 29 | 연결을 얻고, 트랜잭션을 연 뒤 데이터베이스와 상호작용하는 일들은 자동으로 커밋되지 않습니다. 30 | 커밋을 하려면 다음처럼 **`Connection.commit()`** 을 호출해야 합니다. 31 | 32 | ```python 33 | >>> with engine.connect() as conn: 34 | ... # 테이블을 생성합니다. 35 | ... conn.execute(text("CREATE TABLE some_table (x int, y int)")) 36 | ... # 데이터를 삽입합니다. 37 | ... conn.execute( 38 | ... text("INSERT INTO some_table (x, y) VALUES (:x, :y)"), 39 | ... [{"x": 1, "y": 1}, {"x": 2, "y": 4}] 40 | ... ) 41 | ... # 위 변경사항을 커밋합니다. 42 | ... conn.commit() 43 | ``` 44 | 45 | 위 코드의 실행하면 다음과 같은 결과가 출력됩니다. 46 | 47 | ```sql 48 | BEGIN (implicit) 49 | CREATE TABLE some_table (x int, y int) 50 | [...] () 51 | 52 | INSERT INTO some_table (x, y) VALUES (?, ?) 53 | [...] ((1, 1), (2, 4)) 54 | 55 | COMMIT 56 | ``` 57 | 58 | **`Engine.begin()`** 으로 트랜잭션 종료시 자동으로 커밋을 찍게할 수도 있습니다. 59 | 60 | ```python 61 | >>> with engine.begin() as conn: 62 | ... conn.execute( 63 | ... text("INSERT INTO some_table (x, y) VALUES (:x, :y)"), 64 | ... [{"x": 6, "y": 8}, {"x": 9, "y": 10}] 65 | ... ) 66 | ... # 트랜잭션 (컨텍스트 구문)이 끝나면 커밋됩니다. 67 | ``` 68 | 69 | 위 코드의 실행하면 다음과 같은 결과가 출력됩니다. 70 | 71 | ```sql 72 | BEGIN (implicit) 73 | INSERT INTO some_table (x, y) VALUES (?, ?) 74 | [...] ((6, 8), (9, 10)) 75 | 76 | COMMIT 77 | ``` 78 | 79 |
80 | 81 | ## 명령문 실행의 기초 82 | 83 | 다음처럼 쿼리를 실행하고 그 결과를 받아올 수 있습니다. 84 | 85 | ```python 86 | >>> with engine.connect() as conn: 87 | ... # conn.execute() 는 Result라는 객체에 내보냅니다. 88 | ... result = conn.execute(text("SELECT x, y FROM some_table")) 89 | ... for row in result: 90 | ... print(f"x: {row.x} y: {row.y}") 91 | 92 | x: 1 y: 1 93 | x: 2 y: 4 94 | x: 6 y: 8 95 | x: 9 y: 10 96 | ``` 97 | 98 | - [`Result`](https://docs.sqlalchemy.org/en/14/core/connections.html#sqlalchemy.engine.Result) 객체는 `conn.execute()` 가 반환해주는 **"쿼리 결과"를 들고 있는 객체**입니다. 99 | - 링크를 눌러보면 어떤 기능을 제공하는지 볼 수 있습니다. 100 | - 예를 들면 `Result.all()` 을 통해 [`Row`](https://docs.sqlalchemy.org/en/14/core/connections.html#sqlalchemy.engine.Row) 객체의 리스트를 받을 수 있습니다. 101 | 102 | > `Result` 와 `Row` 모두 sqlalchemy에서 제공하는 객체입니다. 103 | 104 | `Result` 객체로 다음처럼 각 행에 액세스할 수 있습니다. 105 | 106 | ```python 107 | result = conn.execute(text("select x, y from some_table")) 108 | 109 | # 튜플로 접근합니다. 110 | for x, y in result: 111 | # ... 112 | 113 | # 정수 인덱스로 접근합니다. 114 | for row in result: 115 | x = row[0] 116 | 117 | # 속성 이름으로 접근합니다. 118 | for row in result: 119 | y = row.y 120 | 121 | # 매핑 액세스로 접근합니다. 122 | for dict_row in result.mappings(): 123 | x = dict_row['x'] 124 | y = dict_row['y'] 125 | ``` 126 | 127 |
128 | 129 | ## 쿼리에 매개 변수 전달하기 130 | 131 | 쿼리에 다음처럼 파라미터를 전달할 수 있습니다. 132 | 133 | ```python 134 | >>> with engine.connect() as conn: 135 | ... result = conn.execute( 136 | ... text("SELECT x, y FROM some_table WHERE y > :y"), # 콜론 형식(:)으로 받습니다. 137 | ... {"y": 2} # 사전 형식으로 넘깁니다. 138 | ... ) 139 | ... for row in result: 140 | ... print(f"x: {row.x} y: {row.y}") 141 | 142 | x: 2 y: 4 143 | x: 6 y: 8 144 | x: 9 y: 10 145 | ``` 146 | 147 | 다음처럼 여러 개의 매개 변수를 보낼수도 있습니다. 148 | 149 | ```python 150 | >>> with engine.connect() as conn: 151 | ... conn.execute( 152 | ... text("INSERT INTO some_table (x, y) VALUES (:x, :y)"), 153 | ... [{"x": 11, "y": 12}, {"x": 13, "y": 14}] # 사전의 리스트로 넘깁니다. 154 | ... ) 155 | ... conn.commit() 156 | ``` 157 | 158 | 위 코드는 다음과 같은 쿼리를 실행하게 됩니다. 159 | 160 | ```sql 161 | INSERT INTO some_table (x, y) VALUES (?, ?) [...] ((11, 12), (13, 14)) 162 | ``` 163 | 164 | > ["명령문으로 매개 변수 묶기"](https://docs.sqlalchemy.org/en/14/tutorial/dbapi_transactions.html#bundling-parameters-with-a-statement) 가 공식 문서에 나오지만, 이 부분은 저도 이해가 되지 않아 넘기겠습니다. 165 | > 추후 이해하신 분은 이 문서에 기여해주시면 감사하겠습니다. 166 | 167 |
168 | 169 | ## ORM `Session`으로 실행 170 | 171 | 이번에는 `Connection` 객체가 아니라 ORM에서 제공해주는 `Session` 로 쿼리를 실행해봅시다. 172 | 다음처럼 해볼 수 있습니다. 173 | 174 | ```python 175 | >>> from sqlalchemy.orm import Session 176 | 177 | >>> stmt = text("SELECT x, y FROM some_table WHERE y > :y ORDER BY x, y").bindparams(y=6) 178 | 179 | >>> # Session 객체에 Engine 객체의 인스턴스를 넘겨 데이터베이스와 상호작용할 수 있는 인스턴스를 얻습니다. 180 | >>> with Session(engine) as session: 181 | ... # Session.execute() 메서드로 쿼리를 실행합니다. 182 | ... result = session.execute(stmt) 183 | ... for row in result: 184 | ... print(f"x: {row.x} y: {row.y}") 185 | ``` 186 | 187 | `Session` 역시 종료 시에 자동으로 커밋을하지 않습니다. 커밋을 하려면 다음처럼 직접 `Session.commit()` 을 호출해야 합니다. 188 | 189 | ```python 190 | >>> with Session(engine) as session: 191 | ... result = session.execute( 192 | ... text("UPDATE some_table SET y=:y WHERE x=:x"), 193 | ... [{"x": 9, "y":11}, {"x": 13, "y": 15}] 194 | ... ) 195 | ... session.commit() # 명시적으로 호출해야 합니다. 196 | ``` 197 | 198 | -------------------------------------------------------------------------------- /src/tutorial/4. 데이터베이스 메타데이터로 작업하기.md: -------------------------------------------------------------------------------- 1 | # 데이터베이스 메타데이터로 작업하기 2 | 3 |
4 | 5 | SQLAlchemy Core와 ORM은 파이썬 객체를 데이터베이스의 테이블과 컬럼처럼 사용할 수 있게 하기 위해서 만들어졌습니다. 이러한 것들을 데이터베이스 메타데이터로 사용할 수 있습니다. 6 | 7 | > 메타데이터는 데이터를 기술하는 데이터를 설명합니다. 여기서 **메타데이터는 구성된 테이블, 열, 제약 조건 및 기타 객체 정보 등을 말합니다.** 8 | 9 |
10 | 11 | ## 테이블 객체를 만들고 메타데이터에 담기 12 | 13 | 관계형 데이터베이스에서는 쿼리를 통해 테이블을 만들지만, SQLAlchemy에서는 Python 객체를 통해 테이블을 만들 수 있습니다. 14 | SQLAlchemy 표현 언어를 시작할려면 사용할려는 데이터베이스 테이블을 `Table` 객체로 만들어줘야합니다. 15 | 16 | ```python 17 | >>> from sqlalchemy import MetaData 18 | >>> metadata = MetaData() # 테이블들의 메타 정보를 담게될 객체입니다. 19 | >>> 20 | >>> from sqlalchemy import Table, Column, Integer, String 21 | >>> user_table = Table( 22 | ... 'user_account', # 데이터베이스에 저장될 table 이름입니다. 23 | ... metadata, 24 | ... Column('id', Integer, primary_key=True), # 이 테이블에 들어갈 컬럼입니다. 25 | ... Column('name', String(30)), 26 | ... Column('fullname', String), 27 | ... ) 28 | ``` 29 | 30 | - `Table` 객체를 통해 데이터베이스 테이블을 만들 수 있습니다. 31 | - `Column`을 통해 테이블의 컬럼을 구현합니다. 32 | - 기본적으로 `Column(컬럼 이름, 데이터 유형)` 와 같이 정의합니다. 33 | 34 | `Table` 인스턴스를 만들고나면 다음처럼 만들어진 컬럼 정보를 알 수 있습니다. 35 | 36 | ```Python 37 | >>> user_table.c.name 38 | Column('name', String(length=30), table=) 39 | 40 | >>> user_table.c.keys() 41 | ['id', 'name', 'fullname'] 42 | ``` 43 | 44 |
45 | 46 | ## 단순 제약 선언하기 47 | 48 | 우리는 위의 user 테이블을 만드는 코드에서 `Column('id', Integer, primary_key=True)` 구문을 보았습니다. 49 | 이는 `id` 컬럼을 기본키로 둔다고 선언하는 것입니다. 50 | 기본키는 암시적으로 `PrimaryKeyConstraint` 객체에 구조로 선언되어있습니다. 이는 다음처럼 확인할 수 있습니다. 51 | 52 | ```python 53 | >>> user_table.primary_key 54 | PrimaryKeyConstraint(Column('id', Integer(), table=, primary_key=True, nullable=False)) 55 | ``` 56 | 57 | 기본키와 더불어 외래키도 다음처럼 선언할 수 있습니다. 58 | 59 | ```python 60 | >>> from sqlalchemy import ForeignKey 61 | >>> address_table = Table( 62 | ... "address", 63 | ... metadata, 64 | ... Column('id', Integer, primary_key=True), 65 | ... Column('user_id', ForeignKey('user_account.id'), nullable=False), # ForeignKey 객체로 외래 키를 선언합니다. 66 | ... Column('email_address', String, nullable=False) 67 | ... ) 68 | ``` 69 | 70 | - `ForeignKey('테이블 이름.외래 키')` 형태로 외래 키 컬럼을 선언할 수 있습니다. 71 | - 이 때 `Column` 객체의 데이터타입을 생략할 수 있습니다. 데이터타입은 외래 키에 해당하는 컬럼을 찾아서 자동으로 추론됩니다. 72 | - 따로 설명하지 않았지만, `nullable=False` 파라미터와 값을 넘김으로써 컬럼에 `NOT NULL` 제약 조건을 선언할 수 있습니다. 73 | 74 |
75 | 76 | ## 데이터베이스에 적용하기 77 | 78 | 지금까지 SQLAlchemy로 데이터베이스 테이블을 선언했습니다. 이제 이렇게 선언한 테이블이 실제 데이터베이스에 생성되도록 해봅시다. 79 | 다음처럼 `metadata.create_all()` 을 실행합니다. 80 | 81 | ```python 82 | >>> metadata.create_all(engine) 83 | 84 | # 위 코드는 `metadata` 인스턴스에 기록된 모든 테이블들을 생성합니다. 85 | # 결과적으로는 아래 쿼리를 실행하게 됩니다. 86 | 87 | BEGIN (implicit) 88 | PRAGMA main.table_...info("user_account") 89 | ... 90 | PRAGMA main.table_...info("address") 91 | ... 92 | CREATE TABLE user_account ( 93 | id INTEGER NOT NULL, 94 | name VARCHAR(30), 95 | fullname VARCHAR, 96 | PRIMARY KEY (id) 97 | ) 98 | ... 99 | CREATE TABLE address ( 100 | id INTEGER NOT NULL, 101 | user_id INTEGER NOT NULL, 102 | email_address VARCHAR NOT NULL, 103 | PRIMARY KEY (id), 104 | FOREIGN KEY(user_id) REFERENCES user_account (id) 105 | ) 106 | ... 107 | COMMIT 108 | ``` 109 | 110 |
111 | 112 | ## ORM 방식으로 테이블 메타데이터 정의하기 113 | 114 | 위의 데이터베이스 구조를 만들고 제약조건을 똑같이 사용하지만, 이번에는 ORM 방식으로 진행해보겠습니다. 115 | 116 |
117 | 118 | ### 레지스트리 설정하기 119 | 120 | 먼저 다음처럼 `registry` 객체를 만듭니다. 121 | 122 | ```python 123 | >>> from sqlalchemy.orm import registry 124 | >>> mapper_registry = registry() 125 | ``` 126 | 127 | `registery` 객체는 `MetaData` 객체를 포함하고 있습니다. 128 | 129 | ```python 130 | >>> mapper_registry.metadata 131 | MetaData() 132 | ``` 133 | 134 | 이제 다음을 실행합니다. 135 | 136 | ```python 137 | >>> Base = mapper_registry.generate_base() 138 | ``` 139 | 140 | > 위 과정을 다음처럼 `declarative_base` 로 한 번에 할 수 있습니다. 141 | > 142 | > ```python 143 | > >>> from sqlalchemy.orm import declarative_base 144 | > >>> Base = declarative_base() 145 | > ``` 146 | 147 |
148 | 149 | ### ORM 객체 선언하기 150 | 151 | `Base` 객체를 상속받는 하위 객체를 정의함으로써 ORM 방식으로 데이터베이스의 테이블을 선언할 수 있습니다. 152 | 153 | ```python 154 | >>> from sqlalchemy.orm import relationship 155 | >>> class User(Base): 156 | ... __tablename__ = 'user_account' # 데이터베이스에서 사용할 테이블 이름입니다. 157 | ... 158 | ... id = Column(Integer, primary_key=True) 159 | ... name = Column(String(30)) 160 | ... fullname = Column(String) 161 | ... 162 | ... addresses = relationship("Address", back_populates="user") 163 | ... 164 | ... def __repr__(self): 165 | ... return f"User(id={self.id!r}, name={self.name!r}, fullname={self.fullname!r})" 166 | 167 | >>> class Address(Base): 168 | ... __tablename__ = 'address' 169 | ... 170 | ... id = Column(Integer, primary_key=True) 171 | ... email_address = Column(String, nullable=False) 172 | ... user_id = Column(Integer, ForeignKey('user_account.id')) 173 | ... 174 | ... user = relationship("User", back_populates="addresses") 175 | ... 176 | ... def __repr__(self): 177 | ... return f"Address(id={self.id!r}, email_address={self.email_address!r})" 178 | ``` 179 | 180 | `User`, `Address` 객체는 `Table` 객체를 포함합니다. 181 | 다음처럼 `__table__` 속성을 통해 확인할 수 있습니다. 182 | 183 | ```python 184 | >>> User.__table__ 185 | Table('user_account', MetaData(), 186 | Column('id', Integer(), table=, primary_key=True, nullable=False), 187 | Column('name', String(length=30), table=), 188 | Column('fullname', String(), table=), schema=None) 189 | ``` 190 | 191 |
192 | 193 | ### ORM 객체 생성하기 194 | 195 | 위에서 정의한 뒤, 다음처럼 `ORM` 객체를 생성할 수 있습니다. 196 | 197 | ```python 198 | >>> sandy = User(name="sandy", fullname="Sandy Cheeks") 199 | >>> sandy 200 | User(id=None, name='sandy', fullname='Sandy Cheeks') 201 | ``` 202 | 203 |
204 | 205 | ### 데이터베이스에 적용하기 206 | 207 | 이제 다음처럼 ORM으로 선언한 테이블을 실제로 데이터베이스에 적용되도록 할 수 있습니다. 208 | 209 | ```python 210 | >>> mapper_registry.metadata.create_all(engine) 211 | >>> Base.metadata.create_all(engine) 212 | ``` 213 | 214 |
215 | 216 | ## 기존 데이터베이스의 테이블을 ORM 객체로 불러오기 217 | 218 | 위의 모든 방법들은 테이블을 직접 선언하지않고, 데이터베이스에 테이블을 가져오는 방법이 있습니다. 219 | 코드는 아래와 같습니다. 220 | 221 | ```python 222 | >>> some_table = Table("some_table", metadata, autoload_with=engine) 223 | 224 | BEGIN (implicit) 225 | PRAGMA main.table_...info("some_table") 226 | [raw sql] () 227 | SELECT sql FROM (SELECT * FROM sqlite_master UNION ALL SELECT * FROM sqlite_temp_master) WHERE name = ? AND type = 'table' 228 | [raw sql] ('some_table',) 229 | PRAGMA main.foreign_key_list("some_table") 230 | ... 231 | PRAGMA main.index_list("some_table") 232 | ... 233 | ROLLBACK 234 | ``` 235 | 236 | 이제 다음과 같이 사용할 수 있습니다. 237 | 238 | ```python 239 | >>> some_table 240 | Table('some_table', MetaData(), 241 | Column('x', INTEGER(), table=), 242 | Column('y', INTEGER(), table=), 243 | schema=None) 244 | ``` 245 | -------------------------------------------------------------------------------- /src/tutorial/5.1. Core와 ORM 방식으로 행 조회하기.md: -------------------------------------------------------------------------------- 1 | # Core와 ORM 방식으로 행 조회하기 2 | 3 |
4 | 5 | 이번 챕터에서는 SQLAlchemy에서 가장 자주 쓰이는 Select에 대해서 다룹니다. 6 | 7 |
8 | 9 | ## `select()` 를 통한 SQL 표현식 구성 10 | 11 | `select()` 생성자는 `insert()` 생성자와 같은 방식으로 쿼리문을 만들 수 있습니다. 12 | 13 | ```python 14 | >>> from sqlalchemy import select 15 | >>> stmt = select(user_table).where(user_table.c.name == 'spongebob') 16 | >>> print(stmt) 17 | """ 18 | SELECT user_account.id, user_account.name, user_account.fullname 19 | FROM user_account 20 | WHERE user_account.name = :name_1 21 | """ 22 | ``` 23 | 24 | 마찬가지로 쿼리문을 실행시키 위해 같은 레벨의 SQL 생성자들(select, insert, update, create등)처럼 `Connection.execute()` 메서드에 쿼리를 넣어 실행시킬 수 있습니다. 25 | 26 | ```python 27 | >>> with engine.connect() as conn: 28 | ... for row in conn.execute(stmt): 29 | ... print(row) 30 | (1, 'spongebob', 'Spongebob Squarepants') 31 | ``` 32 | 33 | 한편 ORM을 사용해 `select` 쿼리문을 실행시키고 싶을 때는 `Session.exeucte()`를 사용해야합니다. 34 | 결과는 방금 전의 예제와 마찬가지로 `Row`객체를 반환하지만 이 객체는 이전의 튜토리얼, [4. 데이터베이스 메타데이터로 작업하기](https://github.com/SoogoonSoogoonPythonists/sqlalchemy-for-pythonist/blob/main/tutorial/4.%20%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4%20%EB%A9%94%ED%83%80%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%A1%9C%20%EC%9E%91%EC%97%85%ED%95%98%EA%B8%B0.md#%ED%85%8C%EC%9D%B4%EB%B8%94%EC%97%90-%EB%A7%A4%ED%95%91%ED%95%A0-%ED%81%B4%EB%9E%98%EC%8A%A4-%EC%84%A0%EC%96%B8)에서 35 | 정의했던 `User`객체를 포함하고 있습니다. 36 | 37 | ```python 38 | >>> stmt = select(User).where(User.name == 'spongebob') 39 | # User 객체의 인스턴스 안에 있는 각 row 들을 출력 40 | >>> with Session(engine) as session: 41 | ... for row in session.execute(stmt): 42 | ... print(row) 43 | (User(id=1, name='spongebob', fullname='Spongebob Squarepants'),) 44 | ``` 45 | 46 |
47 | 48 | ## FROM절과 컬럼 세팅하기 49 | 50 | `select()`함수는 위치인자로 `Column`이나 ` Table`등을 포함한 다양한 객체들을 받을 수 있습니다. 51 | 이러한 인자 값들은 `select()`함수의 반환 값, 즉 SQL쿼리문으로 표현 될 수있고 `FROM`절을 세팅해주기도 합니다. 52 | 53 | 54 | ```python 55 | >>> print(select(user_table)) 56 | """ 57 | SELECT user_account.id, user_account.name, user_account.fullname 58 | FROM user_account 59 | """ 60 | ``` 61 | 62 | 각각의 컬럼들을 `Core`를 이용해 조회하려면 `Table.c`접근자를 통해 `Column`객체에 접근하면 됩니다. 63 | 64 | ```python 65 | >>> print(select(user_table.c.name, user_table.c.fullname)) 66 | """ 67 | SELECT user_account.name, user_account.fullname 68 | FROM user_account 69 | """ 70 | ``` 71 | 72 |
73 | 74 | 75 | ### ORM 엔터티 및 열 조회 76 | 77 | SQL 쿼리를 SqlAlchemy에서 구현할 때 테이블이나 컬럼을 표현하기 위해 `User`객체같은 ORM 엔터티나, `User.name`과 같이 컬럼이 매핑된 속성(어트리뷰트)을 사용할 수 있습니다. 78 | 아래의 예제는 `User`엔터티를 조회하는 예제이지만 사실은 79 | `user_table` 를 사용했을 때와 결과가 동일합니다. 80 | 81 | ```python 82 | >>> print(select(User)) 83 | """ 84 | SELECT user_account.id, user_account.name, user_account.fullname 85 | FROM user_account 86 | """ 87 | ``` 88 | 89 | 위의 예제의 쿼리를 ORM `Session.exeucte()`를 통해 똑같이 실행 할 수 있는데, 이때는 `User`엔터티를 조회하는 것과 `user_info`를 조회하는 것에 차이가 있습니다. 90 | `user_info`를 조회하든 `User`엔티티를 조회 하든 둘 다`Row`객체가 반환되지만 ` User`엔터티를 조회 할 경우에는 91 | `User`인스턴스를 포함한 `Row` 객체가 반환됩니다. 92 | 93 | > 여기서 `user_table`과 `User`는 [이전 챕터]()에서 만들어졌는데, 94 | > `user_table`은 `Table` 객체이고 95 | > `User`는 `Base` 객체를 상속받아`Table`객체를 포함하고 있는 엔터티입니다. 96 | 97 | ```python 98 | >>> with Session(engine) as session: 99 | ... row = session.execute(select(User)).first() 100 | ... print(row) 101 | (User(id=1, name='spongebob',fullname='Spongebob Squarepants'),) 102 | ``` 103 | 104 | 한편 객체 속성(class-bound attributes)을 사용해 원하는 컬럼들을 조회할 수도 있습니다. 105 | ```python 106 | >>> print(select(User.name, User.fullname)) 107 | """ 108 | SELECT user_account.name, user_account.fullname 109 | FROM user_account 110 | """ 111 | ``` 112 | 113 | 객체 속성을 `Session.execute()`을 이용해 조회 할 경우에는 114 | 인자로 보내진 객체 속성의 값(컬럼 값)이 아래와 같이 반환됩니다. 115 | ```python 116 | >>> with Session(engine) as session: 117 | ... row = session.execute(select(User.name, User.fullname)).first() 118 | ... print(row) 119 | ('spongebob', 'Spongebob Squarepants') 120 | ``` 121 | 122 | 이러한 방법들은 믹스인 방법으로 섞어서 사용할 수도 있습니다. 123 | ```python 124 | >>> session.execute( 125 | ... select(User.name, Address). 126 | ... where(User.id==Address.user_id). 127 | ... order_by(Address.id) 128 | ... ).all() 129 | [('spongebob', Address(id=1, email_address='spongebob@sqlalchemy.org')), 130 | ('sandy', Address(id=2, email_address='sandy@sqlalchemy.org')), 131 | ('sandy', Address(id=3, email_address='sandy@squirrelpower.org'))] 132 | ``` 133 | 134 |
135 | 136 | ### 라벨링된 SQL 표현식 조회하기 137 | 138 | `SELECT name AS username FROM user_account` 쿼리를 실행 할 경우 아래와 같은 결과가 나옵니다. 139 | 140 | |username| 141 | |------| 142 | |patrick| 143 | |sandy| 144 | |spongebob| 145 | 146 | 여기서 우리는 `name`컬럼의 이름을 `username`으로 라벨링 해줬기 때문에 상단 컬럼에 `username` 이 뜨는 건데요. 147 | 이러한 기능을 SQLAlchemy의 `ColumnElement.label() `함수를 이용해 동일하게 구현할 수 있습니다. 148 | ```python 149 | >>> from sqlalchemy import func, cast 150 | >>> stmt = ( 151 | ... select( 152 | ... ("Username: " + user_table.c.name).label("username"), # 이렇게 라벨링 합니다. 153 | ... ).order_by(user_table.c.name) 154 | ... ) 155 | >>> with engine.connect() as conn: 156 | ... for row in conn.execute(stmt): 157 | ... print(f"{row.username}") # 라벨링한 부분은 이렇게 접근할 수 있습니다. 158 | Username: patrick 159 | Username: sandy 160 | Username: spongebob 161 | ``` 162 |
163 | 164 | ### 문자열 컬럼 조회하기 165 | 166 | 보통 `Select`객체나 `select()` 생성자를 이용 해 컬럼을 조회하는 경우가 많지만 가끔은 임의로 문자열과 함께 컬럼을 조회 해야하는 경우가 있습니다. 이번 섹션에서는 이러한 스트링 데이터를 조회 하는 방법에 다룹니다. 167 | 168 | `text()` 생성자는 이전 챕터 [3. 트랜잭션 및 데이터베이스API 작업](https://github.com/SoogoonSoogoonPythonists/sqlalchemy-for-pythonist/blob/main/tutorial/3.%20%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98%20%EB%B0%8F%20DBAPI%20%EC%9E%91%EC%97%85.md)에서 한 번 소개가 되었는데요, 이 `text()`생성자안에 `SELECT`구문을 곧바로 넣어 사용할 수도 있었습니다. 169 | 170 | 171 | 한편 우리는 `SELECT 'some_phrase', name FROM user_account` 와 같은 쿼리를 실행시키고 싶다고 생각해봅시다. 172 | 이 때, `some_phrase`은 문자열이기 때문에 꼭 큰 따옴표나 작은 따옴표로 감싸줘야합니다. 그리고 그 결과, 출력물에도 어쩔 수 없이 작은 따옴표가 전부 붙어서 나옵니다. 173 | 174 | ```python 175 | >>> from sqlalchemy import text 176 | >>> stmt = ( 177 | ... select( 178 | ... text("'some phrase'"), user_table.c.name 179 | ... ).order_by(user_table.c.name) 180 | ... ) 181 | >>> with engine.connect() as conn: 182 | ... print(conn.execute(stmt).all()) 183 | [('some phrase', 'patrick'), ('some phrase', 'sandy'), ('some phrase', 'spongebob')] 184 | ``` 185 | 그래서 보통은 `text()`보다 `literal_column()`을 사용해 작은 따옴표가 결과물에 붙어져서 나오는 문제를 해결할 수 있습니다. 186 | 여기에서 `text`와 `literal_column()`은 거의 비슷하지만 `literal_column()`은 명시적으로 컬럼을 의미하고, 서브쿼리나 다른 SQL 표현식에서 쓰일 수 있게 라벨링도 할 수 있습니다. 187 | 188 | ```python 189 | >>> from sqlalchemy import literal_column 190 | >>> stmt = ( 191 | ... select( 192 | ... literal_column("'some phrase'").label("p"), user_table.c.name 193 | ... ).order_by(user_table.c.name) 194 | ... ) 195 | >>> with engine.connect() as conn: 196 | ... for row in conn.execute(stmt): 197 | ... print(f"{row.p}, {row.name}") 198 | some phrase, patrick 199 | some phrase, sandy 200 | some phrase, spongebob 201 | 202 | ``` 203 | 204 |
205 | 206 | ## WHERE절 207 | 208 | SQLAlchemy를 사용하면 Python 연산자를 사용하여 `name` = 'thead'` 또는 `user_id > 10` 인 데이터만 출력하는 쿼리를 쉽게 작성할 수 있습니다. 209 | 210 | ```python 211 | >>> print(user_table.c.name == 'squidward') 212 | user_account.name = :name_1 213 | 214 | >>> print(address_table.c.user_id > 10) 215 | address.user_id > :user_id_1 216 | ``` 217 | 218 | `WHERE`절을 만들기 위해 `Select.where()`메서드안에 인자값을 넣어 사용할 수도 있습니다. 219 | 220 | ```python 221 | >>> print(select(user_table).where(user_table.c.name == 'squidward')) 222 | """ 223 | SELECT user_account.id, user_account.name, user_account.fullname 224 | FROM user_account 225 | WHERE user_account.name = :name_1 226 | """ 227 | ``` 228 | 229 | 230 | WHERE 절을 통한 JOIN을 구현해야 할 때, 아래와 같이 작성 가능합니다. 231 | ```python 232 | >>> print( 233 | ... select(address_table.c.email_address). 234 | ... where(user_table.c.name == 'squidward'). 235 | ... where(address_table.c.user_id == user_table.c.id) 236 | ... ) 237 | """ 238 | SELECT address.email_address 239 | FROM address, user_account 240 | WHERE user_account.name = :name_1 AND address.user_id = user_account.id 241 | """ 242 | 243 | # 위와 같은 표현이지만, 아래처럼 where()메서드의 파라미터로 넘기는 방식도 가능합니다. 244 | >>> print( 245 | select(address_table.c.email_address). 246 | ... where( 247 | ... user_table.c.name == 'squidward', 248 | ... address_table.c.user_id == user_table.c.id 249 | ... ) 250 | ... ) 251 | """ 252 | SELECT address.email_address 253 | FROM address, user_account 254 | WHERE user_account.name = :name_1 AND address.user_id = user_account.id 255 | """ 256 | ``` 257 | 258 | `and_()`, `or_()`등의 연결 구문도 구현가능합니다. 259 | 260 | ```python 261 | >>> from sqlalchemy import and_, or_ 262 | >>> print( 263 | ... select(Address.email_address). 264 | ... where( 265 | ... and_( 266 | ... or_(User.name == 'squidward', User.name == 'sandy'), 267 | ... Address.user_id == User.id 268 | ... ) 269 | ... ) 270 | ... ) 271 | """ 272 | SELECT address.email_address 273 | FROM address, user_account 274 | WHERE (user_account.name = :name_1 OR user_account.name = :name_2) 275 | AND address.user_id = user_account.id 276 | """ 277 | ``` 278 | 279 | 단순히 같은지, 아닌지 비교하는 경우(equality) `Select.filter_by()`도 자주 사용됩니다. 280 | ```python 281 | >>> print( 282 | ... select(User).filter_by(name='spongebob', fullname='Spongebob Squarepants') 283 | ... ) 284 | """ 285 | SELECT user_account.id, user_account.name, user_account.fullname 286 | FROM user_account 287 | WHERE user_account.name = :name_1 AND user_account.fullname = :fullname_1 288 | """ 289 | ``` 290 | 291 |
292 | 293 | ## FROM절과 JOIN 명시하기 294 | 295 | 앞에서 언급했지만 `FROM`절은 따로 명시하지 않아도 `select()`메서드의 인자에 넣은 컬럼들로 자동 세팅이 됩니다. 296 | ```python 297 | # 따로 FROM 절을 명시하지 않았지만 FROM 절이 세팅되어 출력됩니다. 298 | >>> print(select(user_table.c.name)) 299 | """ 300 | SELECT user_account.name 301 | FROM user_account 302 | """ 303 | ``` 304 | 305 | 만약 ` select()`의 위치 인자로 서로 다른 두 개의 테이블을 참조하는 컬럼을`,`(컴마)로 구분지어 넣을 수도 있습니다 306 | ```python 307 | >>> print(select(user_table.c.name, address_table.c.email_address)) 308 | """ 309 | SELECT user_account.name, address.email_address 310 | FROM user_account, address 311 | """ 312 | ``` 313 | 314 | 서로 다른 두 개의 테이블을 `JOIN`조인하고 싶다면, 이용할 수 있는 메서드가 두 가지가 있는데요, 315 | 하나는 `Select.join()`메서드로 명시적으로 `JOIN` 할 왼쪽에 들어갈 테이블, 오른쪽에 들어 갈 테이블을 직접 지정할 수 있습니다 316 | ```python 317 | >>> print( 318 | ... select(user_table.c.name, address_table.c.email_address). 319 | ... join_from(user_table, address_table) 320 | ... ) 321 | """ 322 | SELECT user_account.name, address.email_address 323 | FROM user_account JOIN address ON user_account.id = address.user_id 324 | """ 325 | ``` 326 | 327 | 다른 하나는 `Select.join()`메서드로 오른쪽에 들어갈 테이블만 명시적으로 적어주고 328 | 나머지 테이블은 암시적으로 컬럼을 선택할 때 참조하게 합니다. 329 | ```python 330 | # 위와 동일한 표현이지만, 조인할 왼쪽 테이블(user_table)은 암시적으로 표현됩니다. 331 | >>> print( 332 | ... select(user_table.c.name, address_table.c.email_address). 333 | ... join(address_table) 334 | ... ) 335 | """ 336 | SELECT user_account.name, address.email_address 337 | FROM user_account JOIN address ON user_account.id = address.user_id 338 | """ 339 | ``` 340 | 또는 `JOIN` 하는 두 개의 테이블을 조금 더 명시적으로 작성하고 싶거나 `FROM`절에 명시적인 추가 옵션을 주고 싶다면 341 | 아래와 같이 작성 할 수도 있습니다. 342 | ```python 343 | >>> print( 344 | ... select(address_table.c.email_address). 345 | ... select_from(user_table).join(address_table) 346 | ... ) 347 | """ 348 | SELECT address.email_address 349 | FROM user_account JOIN address ON user_account.id = address.user_id 350 | """ 351 | ``` 352 | 353 | `Select.select_from()`을 사용하는 또 다른 경우는 우리가 조회하고 싶은 컬럼들을 통해 암시적으로 `FROM` 절을 세팅할 수 없는 경우입니다. 예를 들면 일반적인 SQL 쿼리문에서 `count(*)`를 조회하기 위해선 SQLAlchemy의 `sqlalchemy.sql.expression.func`를 사용해야합니다. 354 | 355 | 356 | ```python 357 | >>> from sqlalchemy import func 358 | >>> print(select(func.count('*')).select_from(user_table)) 359 | """ 360 | SELECT count(:count_2) AS count_1 361 | FROM user_account 362 | """ 363 | ``` 364 | 365 |
366 | 367 | ### On절 세팅하기 368 | 369 | 그런데 뭔가 이상한게 있었죠? 370 | 사실 이 전의 예제에서 `Select.select_from()`이나 `select.join()`을 이용해 두 테이블을 `JOIN`할 때 암시적으로 `ON`절이 세팅되었답니다. 371 | 왜 자동으로 `ON` 절이 세팅 됐냐면, `user_table`객체와, `address_table` 객체가 ` ForeignKeyConstraint`, 즉 외부키 제약을 갖고 있어서 자동으로 세팅이 된 것입니다. 372 | 373 | 만약에 `Join`의 대상인 두 개의 테이블에서 이러한 제약 key가 없을 경우 `ON`절을 직접 지정해야 합니다. 이러한 기능은 `Select.join()`나 `Select.join_from()`메서드에 파라미터 전달을 통해 명시적으로 `ON`절을 세팅할 수 있습니다. 374 | 375 | ```python 376 | >>> print( 377 | ... select(address_table.c.email_address). 378 | ... select_from(user_table). 379 | ... join(address_table, user_table.c.id == address_table.c.user_id) 380 | ... ) 381 | """ 382 | SELECT address.email_address 383 | FROM user_account JOIN address ON user_account.id = address.user_id 384 | """ 385 | ``` 386 |
387 | 388 | 389 | ### OUTER, FULL Join 390 | 391 | SQLAlchemy에서 `LEFT OUTER JOIN`, `FULL OUTER JOIN`를 구현하려면 392 | `Select.join()`과 `Select.join_from()`메서드의 키워드 인자로 `Select.join.isouter`, 393 | `Select.join.full `를 사용할 수 있습니다. 394 | 395 | 396 | `LEFT OUTER JOIN`을 구현 한 경우, 397 | ```python 398 | >>> print( 399 | ... select(user_table).join(address_table, isouter=True) 400 | ... ) 401 | """ 402 | SELECT user_account.id, user_account.name, user_account.fullname 403 | FROM user_account LEFT OUTER JOIN address ON user_account.id = address.user_id 404 | """ 405 | ``` 406 | `FULL OUTER JOIN`을 구현 한 경우, 407 | ```python 408 | >>> print( 409 | ... select(user_table).join(address_table, full=True) 410 | ... ) 411 | """ 412 | SELECT user_account.id, user_account.name, user_account.fullname 413 | FROM user_account FULL OUTER JOIN address ON user_account.id = address.user_id 414 | """ 415 | ``` 416 | 417 | 418 |
419 | 420 | 421 | ## ORDER BY, GROUP BY, HAVING 422 | 423 | - `ORDER BY`절은 `SELECT`절에서 조회한 행들의 순서를 설정할 수 있습니다. 424 | - `GROUP BY`절은 그룹 함수로 조회된 행들을 특정한 컬럼을 기준으로 그룹을 만듭니다. 425 | - `HAVING`은 `GROUP BY`절을 통해 생성된 그룹에 대해 조건을 겁니다. 426 | 427 |
428 | 429 | ### ORDER BY 430 | 431 | `Select.order_by()`를 이용해 `ORDER BY`기능을 구현할 수 있습니다. 이때 위치 인자 값으로 `Column`객체나 이와 비슷한 객체들을 받을 수 있습니다. 432 | 433 | 434 | ```python 435 | >>> print(select(user_table).order_by(user_table.c.name)) 436 | """ 437 | SELECT user_account.id, user_account.name, user_account.fullname 438 | FROM user_account ORDER BY user_account.name 439 | """ 440 | ``` 441 | 오름차순, 내림차순은 442 | `ColumnElement.asc()`, `ColumnElement.desc()` 제한자를 통해 구현 할 수 있습니다. 443 | 아래 예제는 `user_account.fullname`컬럼 기준으로 내림 차순으로 정렬합니다. 444 | ```python 445 | >>> print(select(User).order_by(User.fullname.desc())) 446 | """ 447 | SELECT user_account.id, user_account.name, user_account.fullname 448 | FROM user_account ORDER BY user_account.fullname DESC 449 | """ 450 | ``` 451 |
452 | 453 | ### 그룹함수 : GROUP BY, Having 454 | 455 | SQL에서는 집계함수를 이용해 조회 된 여러개의 행들을 하나의 행으로 합칠 수도 있습니다. 456 | 집계함수의 예로 `COUNT()`, `SUM()`, `AVG()`등이 있습니다. 457 | 458 | SQLAlchemy에서는 `func`라는 네임스페이스를 이용해 SQL함수를 제공하는데, 이 `func`는 459 | SQL함수의 이름이 주어지면 `Function`인스턴스를 생성합니다. 460 | 461 | 아래의 예제에서는 `user_account.id`컬럼을 SQL `COUNT()`함수에 렌더링 하기 위해 `count()`함수를 호출합니다. 462 | ```python 463 | >>> from sqlalchemy import func 464 | >>> count_fn = func.count(user_table.c.id) 465 | >>> print(count_fn) 466 | """ 467 | count(user_account.id) 468 | """ 469 | ``` 470 | 471 | SQL 함수에 관해서는 [SQL 함수 다루기]()에서 더 자세히 설명되어 있습니다. 472 | 473 | 다시 설명하자면 474 | - `GROUP BY`는 조회된 행들을 특정 그룹으로 나눌 때 필요한 함수입니다. 475 | 만약에 `SELECT` 절에서 몇 개의 컬럼을 조회 할 경우 SQL에서는 직,간접적으로 이 컬럼들이 기본키(primary key)를 기준으로 476 | `GROUP BY`에 종속되도록 합니다. 477 | - `HAVING` 는 `GROUP BY`로 만들어진 그룹들에 대해 조건을 적용 할 경우 필요합니다.(그룹에 대해 조건을 걸기 때문에 `WHERE`절과 비슷합니다) 478 | 479 | SQLAlchemy에서는 ` Select.group_by()`와 `Select.having()`를 이용해 `GROUP BY`와 `HAVING`을 구현 할 수 있습니다. 480 | 481 | ```python 482 | >>> with engine.connect() as conn: 483 | ... result = conn.execute( 484 | ... select(User.name, func.count(Address.id).label("count")). 485 | ... join(Address). 486 | ... group_by(User.name). 487 | ... having(func.count(Address.id) > 1) 488 | ... ) 489 | ... print(result.all()) 490 | """ 위 구문은 아래 SQL을 표현합니다. 491 | SELECT user_account.name, count(address.id) AS count 492 | FROM user_account JOIN address ON user_account.id = address.user_id GROUP BY user_account.name 493 | HAVING count(address.id) > ? 494 | [...] (1,) 495 | """ 496 | 497 | [('sandy', 2)] 498 | ``` 499 |
500 | 501 | 502 | ### 별칭을 통해 그룹화 또는 순서 정렬하기 503 | 504 | 505 | 어떤 데이터 베이스 백엔드에서는 집계함수를 사용해 테이블을 조회 할 때 `ORDER BY` 절이나 `GROUP BY`절에 이미 명시된 집계함수를 **다시** 명시적으로 사용하지 않는 것이 중요합니다. 506 | ```sql 507 | # NOT GOOD 508 | SELECT id, COUNT(id) FROM user_account GROUP BY id ORDER BY count(id) 509 | 510 | # CORRECT 511 | SELECT id, COUNT(id) as cnt_id FROM user_account GROUP BY id ORDER BY cnt_id 512 | ``` 513 | 따라서 별칭을 통해 `ORDER BY` 나 `GROUP BY`를 구현하려면 514 | `Select.order_by()` 또는 `Select.group_by()`메서드에 인자로 사용 할 별칭을 넣어주면 됩니다. 515 | 여기에 사용된 별칭이 먼저 렌더링 되는건 아니고 컬럼절에 사용된 별칭이 먼저 렌더링 됩니다. 그리고 렌더링된 별칭이 나머지 쿼리문에서 매칭되는게 없다면 에러가 발생합니다. 516 | 517 | 518 | ```python 519 | >>> from sqlalchemy import func, desc 520 | >>> # 컬럼에도 num_addresses 별칭이 들어가고 order_by에도 같은 별칭이 들어갑니다. 521 | >>> stmt = select( 522 | ... Address.user_id, 523 | ... func.count(Address.id).label('num_addresses')).\ 524 | ... group_by("user_id").order_by("user_id", desc("num_addresses")) 525 | >>> print(stmt) 526 | """ 527 | SELECT address.user_id, count(address.id) AS num_addresses 528 | FROM address GROUP BY address.user_id ORDER BY address.user_id, num_addresses DESC 529 | """ 530 | ``` 531 |
532 | 533 | 534 | ## 별칭 사용하기 535 | 536 | 여러개의 테이블을 `JOIN`을 이용해 조회 할 경우 쿼리문에서 테이블 이름을 여러번 적어줘야 하는 경우가 많습니다. 537 | SQL에서는 이러한 문제를 테이블 명이나 서브 쿼리에 별칭(aliases)를 지어 반복되는 부분을 줄일 수 있습니다. 538 | 539 | 한편 SQLAlchemy에서는 이러한 별칭들은 Core의 ` FromCaluse.alias()`함수를 이용해 구현 할 수 있습니다. 540 | `Table`객체 네임스페이스 안에 `Column`객체가 있어 `Table.c`로 컬럼명에 접근할 수 있었는데요. 541 | ```python 542 | print(select(user_table.c.name, user_table.c.fullname)) 543 | """ 544 | SELECT user_account.name, user_account.fullname 545 | FROM user_account 546 | """ 547 | ``` 548 | `Alias`객체 네임스페이스 안에도 `Column`객체가 있어 `Alias.c`로 컬럼에 접근 가능합니다. 549 | ```python 550 | >>> # user_alias_1과 user_alias_2 모두 Alias객체입니다. 551 | >>> user_alias_1 = user_table.alias(‘table1’) 552 | >>> user_alias_2 = user_table.alias(‘table2’) 553 | >>> # 방금 만든 테이블 별명으로 컬럼에 접근하려면 Alias.c.컬럼명으로 접근해야합니다. 554 | >>> print( 555 | ... select(user_alias_1.c.name, user_alias_2.c.name). 556 | ... join_from(user_alias_1, user_alias_2, user_alias_1.c.id > user_alias_2.c.id) 557 | ... ) 558 | 559 | """ 560 | SELECT table1.name, table2.name AS name_1 561 | FROM user_account AS table1 JOIN user_account AS table2 ON table1.id > table2.id 562 | """ 563 | ``` 564 | 565 |
566 | 567 | ### ORM 엔티티 별칭 568 | 569 | ORM도 `FromClause.alias()`메서드와 비슷한 `aliased()`함수가 존재합니다. 570 | 571 | 이 ORM `aliased()`는 ORM의 기능을 유지하면서 원래 매핑된 `Table`객체에 내부적으로 `Alias`객체를 생성합니다. 572 | 573 | > 여기서 `Address`와 `User`객체는 [이전 챕터]()에서 만들어졌는데, 574 | > 두 객체 모두 `Base` 객체를 상속받아`Table`객체를 포함하고 있는 엔터티입니다. 575 | 576 | ```python 577 | >>> user_alias_1 = user_table.alias() 578 | >>> user_alias_2 = user_table.alias() 579 | >>> # 예제에서는 User나 Address엔터티에 적용됩니다. 580 | >>> print( 581 | ... select(User). 582 | ... join_from(User, address_alias_1). 583 | ... where(address_alias_1.email_address == 'patrick@aol.com'). 584 | ... join_from(User, address_alias_2). 585 | ... where(address_alias_2.email_address == 'patrick@gmail.com') 586 | ... ) 587 | """ 588 | SELECT user_account.id, user_account.name, user_account.fullname 589 | FROM user_account JOIN address AS address_1 ON user_account.id = address_1.user_id JOIN address AS address_2 ON user 590 | """ 591 | ``` 592 | 593 |
594 | 595 | ## 서브쿼리와 CTE 596 | 597 | 이 섹션에서는 일반적으로 SELECT의 FROM 절에 있는 서브 쿼리에 대해 설명합니다. 서브쿼리와 유사한 방식으로 사용되지만 추가 기능이 포함된 CTE도 다룹니다. 598 | 599 | > CTE(Common Table Expression)는 동일 쿼리 내에서 여러번 참조할 수 있게 하는 쿼리 내 임시 결과 집합입니다. [이 글](https://blog.sengwoolee.dev/84)에 CTE에 대해 잘 설명되어 있으니, 잘 모르시겠는 분들은 참고하시면 좋습니다. 600 | 601 | SQLAlchemy는 `Subquery` 개체를 602 | `Select.subquery()`사용하여 서브 쿼리를 나타내고 `Select.cte()` 를 사용하여 CTE를 나타냅니다. 603 | 604 | ```python 605 | >>> subq = select( 606 | ... func.count(address_table.c.id).label("count"), 607 | ... address_table.c.user_id 608 | ... ).group_by(address_table.c.user_id).subquery() 609 | >>> print(subq) 610 | """ 611 | SELECT count(address.id) AS count, address.user_id 612 | FROM address GROUP BY address.user_id 613 | """ 614 | 615 | >>> stmt = select( 616 | ... user_table.c.name, 617 | ... user_table.c.fullname, 618 | ... subq.c.count 619 | ... ).join_from(user_table, subq) # ON절은 두 개의 테이블이 이미 foreigh key로 제약이 걸려있어 자동 바인딩된다. 620 | >>> print(stmt) 621 | """ 622 | SELECT user_account.name, user_account.fullname, anon_1.count 623 | FROM user_account JOIN (SELECT count(address.id) AS count, address.user_id AS user_id 624 | FROM address GROUP BY address.user_id) AS anon_1 ON user_account.id = anon_1.user_id 625 | """ 626 | ``` 627 |
628 | 629 | ### 계층 쿼리 630 | SQLAlchemy에서 CTE 구문을 사용하는 방법은 서브 쿼리 구문이 사용되는 방식과 거의 동일합니다. 대신 `Select.subquery()` 메서드의 호출을 `Select.cte()`를 사용하도록 변경하여 결과 객체를 FROM 요소로 사용할 수 있습니다. 631 | 632 | ```python 633 | >>> subq = select( 634 | ... func.count(address_table.c.id).label("count"), 635 | ... address_table.c.user_id 636 | ... ).group_by(address_table.c.user_id).cte() 637 | 638 | >>> stmt = select( 639 | ... user_table.c.name, 640 | ... user_table.c.fullname, 641 | ... subq.c.count 642 | ... ).join_from(user_table, subq) 643 | 644 | >>> print(stmt) 645 | """ 646 | WITH anon_1 AS 647 | (SELECT count(address.id) AS count, address.user_id AS user_id 648 | FROM address GROUP BY address.user_id) 649 | SELECT user_account.name, user_account.fullname, anon_1.count 650 | FROM user_account JOIN anon_1 ON user_account.id = anon_1.user_id 651 | """ 652 | ``` 653 | 654 |
655 | 656 | 657 | ### ORM 엔티티 서브쿼리, CTE 658 | 여기서는 `aliased()`가 `Subquery`, `CTE`서브 쿼리에 대해 동일한 작업을 수행하는 과정을 확인할 수 있습니다. 659 | 660 | ```python 661 | >>> subq = select(Address).where(~Address.email_address.like('%@aol.com')).subquery() 662 | >>> address_subq = aliased(Address, subq) 663 | >>> stmt = select(User, address_subq).join_from(User, address_subq).order_by(User.id, address_subq.id) 664 | >>> with Session(engine) as session: 665 | ... for user, address in session.execute(stmt): 666 | ... print(f"{user} {address}") 667 | 668 | """ 위 구문은 아래 쿼리를 표현합니다. 669 | SELECT user_account.id, user_account.name, user_account.fullname, 670 | anon_1.id AS id_1, anon_1.email_address, anon_1.user_id 671 | FROM user_account JOIN 672 | (SELECT address.id AS id, address.email_address AS email_address, address.user_id AS user_id 673 | FROM address 674 | WHERE address.email_address NOT LIKE ?) AS anon_1 ON user_account.id = anon_1.user_id 675 | ORDER BY user_account.id, anon_1.id 676 | [...] ('%@aol.com',) 677 | """ 678 | 679 | User(id=1, name='spongebob', fullname='Spongebob Squarepants') Address(id=1, email_address='spongebob@sqlalchemy.org') 680 | User(id=2, name='sandy', fullname='Sandy Cheeks') Address(id=2, email_address='sandy@sqlalchemy.org') 681 | User(id=2, name='sandy', fullname='Sandy Cheeks') Address(id=3, email_address='sandy@squirrelpower.org') 682 | ``` 683 | 아래는 CTE생성자를 이용해 위와 같은 사용하는 예제입니다 684 | ```python 685 | >>> cte = select(Address).where(~Address.email_address.like('%@aol.com')).cte() 686 | >>> address_cte = aliased(Address, cte) 687 | >>> stmt = select(User, address_cte).join_from(User, address_cte).order_by(User.id, address_cte.id) 688 | >>> with Session(engine) as session: 689 | ... for user, address in session.execute(stmt): 690 | ... print(f"{user} {address}") 691 | 692 | User(id=1, name='spongebob', fullname='Spongebob Squarepants') Address(id=1, email_address='spongebob@sqlalchemy.org') 693 | User(id=2, name='sandy', fullname='Sandy Cheeks') Address(id=2, email_address='sandy@sqlalchemy.org') 694 | User(id=2, name='sandy', fullname='Sandy Cheeks') Address(id=3, email_address='sandy@squirrelpower.org') 695 | ``` 696 |
697 | 698 | 699 | 700 | ## 스칼라 서브 쿼리, 상호연관 쿼리 701 | 702 | 스칼라 서브 쿼리에 대해 설명하기전 잠시 SQL에서 서브 쿼리에 대해 이야기 하겠습니다. [출처:바이헨 블로그](https://rinuas.tistory.com/entry/%EC%84%9C%EB%B8%8C%EC%BF%BC%EB%A6%ACSub-Query) 703 | 704 | - "서브쿼리"란 하나의 SQL문에 속한 `SELECT`문을 말하고 서브 쿼리의 바깥 쪽에 있는 SQL문을 "메인 쿼리"라고 합니다. 705 | 706 | 이때 서브쿼리의 종류는 메인 쿼리 컬럼을 참조 여부, 서브쿼리의 선언 위치, 서브 쿼리 실행 결과 ROW수에 따라 종류가 나눠집니다 707 | 708 | - 메인 쿼리 컬럼 참조 여부에 따른 구분 709 | - **상호 연관 서브쿼리** : 서브쿼리가 메인 쿼리 컬럼을 참조 710 | - 비상호 연관 서브 쿼리: 서브쿼리가 메인 쿼리의 컬럼을 참조하지 않고 독립적으로 수행하고 메인쿼리에 정보를 전달할 목적으로 사용됩니다. 711 | - 서브쿼리 선언 위치에 따른 구분 712 | - **스칼라 서브 쿼리** : SELECT 문의 컬럼자리에 오는 서브 쿼리(상호 연관 서브쿼리) 713 | - 인라인 뷰 : FROM절 자리에 오는 서브 쿼리 (상호 연관 서브 쿼리) 714 | - 중첩 서브 쿼리 : Where절 자리에 오는 서브 쿼리 (비상호 연관 서브 쿼리) 715 | - 서브 쿼리 실행 결과 ROW수에 따른 구분 716 | - 단일행 서브쿼리(서브 쿼리 연산결과 ROW1개) 717 | - 단중행 서브쿼리(서브 쿼리 연산결과 ROW2개이상):IN, ANY, ALL, EXSITS 718 | 719 |
720 | 721 | SQLAlchemy에서 스칼라 서브 쿼리는 `ColumnElement`객체의 일부인 `ScalarSelect`를 사용하는 방면 일반 서브 쿼리는`FromClause`객체에 있는 `Subquery`를 사용합니다. 722 | 723 | 724 | 스칼라 서브쿼리는 앞에서 설명했던 [그룹 합수](https://github.com/SoogoonSoogoonPythonists/sqlalchemy-for-pythonist/blob/feature/tutorial/5.%20%EB%8D%B0%EC%9D%B4%ED%84%B0%20%ED%95%B8%EB%93%A4%EB%A7%81%ED%95%98%EA%B8%B0%20-%20Select%EA%B5%AC%EB%AC%B8.md#%EA%B7%B8%EB%A3%B9%ED%95%A8%EC%88%98--group-by-having)와 같이 쓰이고는 합니다. 725 | ```python 726 | # Select.scalar_subquery()를 이용해 구현한 스칼라 서브 쿼리 727 | >>> subq = select(func.count(address_table.c.id)). 728 | ... where(user_table.c.id == address_table.c.user_id). 729 | ... scalar_subquery() 730 | >>> print(subq) #ScalarSelect객체 731 | """ 732 | (SELECT count(address.id) AS count_1 733 | FROM address, user_account 734 | WHERE user_account.id = address.user_id) 735 | """ 736 | ``` 737 | 스칼라 서브 쿼리가 `user_account`와 `address`를 FROM절에서 렌더링하지만 738 | 메인쿼리에 있는 `user_account`테이블이 있어서 739 | 스칼라 서브 쿼리에서는 `user_account` 테이블을 렌더링하지 않습니다. 740 | 741 | ```python 742 | >>> stmt = select(user_table.c.name, subq.label("address_count")) 743 | >>> print(stmt) 744 | """ 745 | SELECT user_account.name, (SELECT count(address.id) AS count_1 746 | FROM address 747 | WHERE user_account.id = address.user_id) AS address_count 748 | FROM user_account 749 | """ 750 | ``` 751 | 한편 상호 연관 쿼리를 작성 할 때 테이블 간의 연결이 모호해질 수도 있습니다. 752 | > 튜토리얼에 나와있는 상호 연관쿼리 예제는 제가 이해하지 못했습니다. 753 | > 잘 아시는분은 이문서에 기여 부탁드립니다. 754 | > 755 | 756 |
757 | 758 | 759 | ## UNION, UNION ALL 연산자들 760 | 761 | SQL에서는 `UNION`, `UNION ALL`등으로 두 개의 `SELECT`문을 합치는 것을 의미합니다. 762 | 아래와 같이 쿼리문을 실행 할 수도 있습니다. 763 | ```sql 764 | SELECT id FROM user_account 765 | union 766 | SELECT email_address FROM address 767 | ``` 768 | 그 외에도 집합 연산인 `INTERSECT`(교집합), `EXCEPT`(차집합)도 SQL에서 지원합니다. 769 | SQLAlchemy에서 `Select` 객체에 대하여 `union()`, `intersect()`, `except_()` 혹은 770 | `union_all()`, `intersect_all()`, `except_all()`을 지원합니다. 771 | 772 | 이러한 함수들의 반환 값은 `CompoundSelect`인데 `Select`와 비슷하게 쓰일 수 있는 객체이지만 더 적은 메서드를 갖고 있습니다. 773 | `union_all()`의 반환값 `CompoundSelect`객체는 `Connection.execute()`로 실행될 수 있습니다. 774 | 775 | ```python 776 | >>> from sqlalchemy import union_all 777 | >>> stmt1 = select(user_table).where(user_table.c.name == 'sandy') 778 | >>> stmt2 = select(user_table).where(user_table.c.name == 'spongebob') 779 | >>> u = union_all(stmt1, stmt2) #u는 CompoundSelect 객체입니다. 780 | >>> with engine.connect() as conn: 781 | ... result = conn.execute(u) 782 | ... print(result.all()) 783 | 784 | [(2, 'sandy', 'Sandy Cheeks'), (1, 'spongebob', 'Spongebob Squarepants')] 785 | ``` 786 | 787 | `Subquery`객체를 만들기 위해 `Select`가 `SelectBase.subquery()`메서드를 제공하는 것처럼 788 | `CompoundSelect`객체를 서브 쿼리로 비슷한 방식으로 사용 할 수 있습니다. 789 | ```python 790 | >>> u_subq = u.subquery() 791 | >>> stmt = ( 792 | ... select(u_subq.c.name, address_table.c.email_address). 793 | ... join_from(address_table, u_subq). 794 | ... order_by(u_subq.c.name, address_table.c.email_address) 795 | ... ) 796 | >>> with engine.connect() as conn: 797 | ... result = conn.execute(stmt) 798 | ... print(result.all()) 799 | 800 | [('sandy', 'sandy@sqlalchemy.org'), ('sandy','sandy@squirrelpower.org'), 801 | ('spongebob', 'spongebob@sqlalchemy.org')] 802 | ``` 803 | 804 |
805 | 806 | ## EXISTS 서브쿼리들 807 | 808 | SQLAlchemy는 `SelectBase.exists()`메서드를 통해 `Exists`객체를 만들어 `EXISTS` 구문을 구현합니다. 809 | 810 | ```python 811 | >>> # subq는 Exists객체입니다. 812 | >>> subq = ( 813 | ... select(func.count(address_table.c.id)). 814 | ... where(user_table.c.id == address_table.c.user_id). 815 | ... group_by(address_table.c.user_id). 816 | ... having(func.count(address_table.c.id) > 1) 817 | ... ).exists() 818 | >>> print(subq) 819 | """ 820 | EXISTS (SELECT count(address.id) AS count_1 821 | FROM address, user_account 822 | WHERE user_account.id = address.user_id GROUP BY address.user_id 823 | HAVING count(address.id) > :count_2) 824 | """ 825 | >>> with engine.connect() as conn: 826 | ... result = conn.execute( 827 | ... select(user_table.c.name).where(subq) 828 | ... ) 829 | ... print(result.all()) 830 | 831 | [('sandy',)] 832 | ``` 833 | 834 | 한편 `EXISTS` 구문은 부정으로 사용되지 않는 경우가 더 많습니다. 835 | ```python 836 | # 이메일 주소가 없는 유저네임을 선택하는 쿼리문입니다. 837 | # "~" 연산이 들어간 부분을 확인해보세요 838 | >>> subq = ( 839 | ... select(address_table.c.id). 840 | ... where(user_table.c.id == address_table.c.user_id) 841 | ... ).exists() 842 | >>> stmt = select(user_table.c.name).where(~subq) 843 | >>> print(stmt) 844 | """ 845 | SELECT user_account.id 846 | FROM user_account 847 | WHERE NOT (EXISTS (SELECT count(address.id) AS count_1 848 | FROM address 849 | WHERE user_account.id = address.user_id GROUP BY address.user_id 850 | HAVING count(address.id) > :count_2)) 851 | """ 852 | >>> with engine.connect() as conn: 853 | ... result = conn.execute(stmt) 854 | ... print(result.all()) 855 | 856 | [('patrick',)] 857 | ``` 858 | 859 |
860 | 861 | 862 | ## SQL 함수 다뤄보기 863 | 864 | 이 섹션 앞부분의 [그룹함수 :GROUP BY, HAVING]()에서 처음 소개된 `func` 객체는 865 | 새로운 `Function` 객체를 생성하기 위한 팩토리 역할을 합니다. 866 | `select()`와 같은 구문을 사용 할때는 인자 값으로 `func`객체로 생성된 SQL함수를 받을 수 있습니다. 867 | 868 | 869 | - `count()` : 집계함수로 행의 개수를 출력하는데 사용됩니다. 870 | ```python 871 | >>> # cnt는 타입입니다. 872 | >>> cnt = func.count() 873 | >>> print(select(cnt).select_from(user_table)) 874 | """ 875 | SELECT count(*) AS count_1FROM user_account 876 | """ 877 | ``` 878 | - `lower()` : 문자열 함수로 문자열을 소문자로 바꿔줍니다. 879 | ```python 880 | >>> print(select(func.lower("A String With Much UPPERCASE"))) 881 | """ 882 | SELECT lower(:lower_2) AS lower_1 883 | """ 884 | ``` 885 | - `now()` : 현재 시간과 날짜를 반환해주는 함수입니다. 886 | 이 함수는 굉장히 흔하게 사용되는 함수이기에 SQLAlchemy는 서로 다른 백엔드에서 손쉽게 렌더링 할 수 있도록 도와줍니다. 887 | ```` python 888 | >>> stmt = select(func.now()) 889 | >>> with engine.connect() as conn: 890 | ... result = conn.execute(stmt) 891 | ... print(result.all()) 892 | 893 | [(datetime.datetime(...),)] 894 | ```` 895 | 896 | 다양한 데이터베이스 백엔드에서는 서로 다른 이름의 SQL함수를 갖고 있습니다. 897 | 따라서 `func` 는 가능한 자유롭게 어떤 이름이든 `func`의 네임스페이스에 접근 할 수 있도록 허용합니다. 그리고 그 이름을 자동으로 SQL 함수로 받아들여 렌더링 합니다. 898 | ```python 899 | >>> # crazy_function의 데이터 타입은 Function입니다. 900 | >>> crazy_function = func.some_crazy_function(user_table.c.fullname, 17) 901 | >>> print(select(crazy_function)) 902 | """ 903 | SELECT some_crazy_function(user_account.name, :some_crazy_function_2) AS some_crazy_function_1 904 | FROM user_account 905 | """ 906 | ``` 907 | 한편 SQLAlchemy에서는 SQL에서 일반적으로 자주 쓰이는 `count`, `now`, `max` , `concat`같은 SQL 함수를 백엔드별로 적절한 데이터 타입을 제공합니다. 908 | 909 | ```python 910 | >>> from sqlalchemy.dialects import postgresql 911 | >>> print(select(func.now()).compile(dialect=postgresql.dialect())) 912 | """ 913 | SELECT now() AS now_1 914 | """ 915 | 916 | >>> from sqlalchemy.dialects import oracle 917 | >>> print(select(func.now()).compile(dialect=oracle.dialect())) 918 | """ 919 | SELECT CURRENT_TIMESTAMP AS now_1 FROM DUAL 920 | """ 921 | ``` 922 | 923 |
924 | 925 | ### Functions Have Return Types 926 | > 원문의 Functions Have Return Types 부분은 제가 이해하지 못했습니다. 927 | > 이에 대해 이해하신 분 있으시다면 이 부분에 기여 부탁드립니다. 감사합니다. 928 | 929 |
930 | 931 | ### Built-in Functions Have Pre-Configured Return Types 932 | > 원문의 Built-in Functions Have Pre-Configured Return Types 부분은 제가 이해하지 못했습니다. 933 | > 이에 대해 이해하신 분 있으시다면 이 부분에 기여 부탁드립니다. 감사합니다. 934 | 935 | 936 |
937 | 938 | ### 윈도우 함수 939 | 940 | 윈도우 함수는 GROUP BY와 비슷한 함수이고 행간의 관계를 쉽게 정의 하기 위해 만든 함수입니다. 941 | 윈도우 함수에 대해 알고 싶으신 분들은 [민지님 블로그](https://mizykk.tistory.com/121)에 자세한 설명이 나와있으니 한 번 읽어보시고 아래의 내용들을 이어서 읽어주세요. 942 | 943 | SQLAlchemy에서는, `func` 네임스페이스에 의해 생성된 모든 SQL 함수 중 하나로 944 | OVER 구문을 구현하는 `FunctionElement.over()` 메서드가 있습니다. 945 | 946 | 947 | 윈도우 함수 중 하나로 행의 개수를 세는 `row_number()` 함수가 있습니다. 948 | 각 행을 사용자 이름대로 그룹을 나누고 그 안에서 이메일 주소에 번호를 매길 수 있습니다. 949 | 950 | ```python 951 | # FunctionElement.over.partition_by파라미터를 사용하여 952 | # PARTITION BY 절이 OVER 절에 렌더링되도록 했습니다. 953 | >>> stmt = select( 954 | ... func.row_number().over(partition_by=user_table.c.name), 955 | ... user_table.c.name, 956 | ... address_table.c.email_address 957 | ... ).select_from(user_table).join(address_table) 958 | >>> with engine.connect() as conn: 959 | ... result = conn.execute(stmt) 960 | ... print(result.all()) 961 | [(1, 'sandy', 'sandy@sqlalchemy.org'), 962 | (2, 'sandy', 'sandy@squirrelpower.org'), 963 | (1, 'spongebob', 'spongebob@sqlalchemy.org')] 964 | ``` 965 | `FunctionElement.over.order_by`를 사용하여 `ORDER BY` 절을 사용할 수도 있습니다. 966 | ```python 967 | >>> stmt = select( 968 | ... func.count().over(order_by=user_table.c.name), 969 | ... user_table.c.name, 970 | ... address_table.c.email_address).select_from(user_table).join(address_table) 971 | >>> with engine.connect() as conn: 972 | ... result = conn.execute(stmt) 973 | ... print(result.all()) 974 | 975 | [(2, 'sandy', 'sandy@sqlalchemy.org'), 976 | (2, 'sandy', 'sandy@squirrelpower.org'), 977 | (3, 'spongebob', 'spongebob@sqlalchemy.org')] 978 | ``` 979 |
980 | 981 | ### WITHIN GROUP, FILTER등 특수한 지정자 982 | 983 | `WITHIN GORUP`이라는 SQL 구문은 순서 집합 또는 가상 집합 그리고 집계함수와 함께 쓰입니다. 984 | 일반적인 순서 집합 함수는 `percentile_cont()` 그리고 `rank()`를 포함하고 있습니다. 985 | SQLAlchemy에서는 `rank`, `dense_rank`, `percentile_count`, `percentile_disc`가 구현되어 있고 986 | 각각은 `FunctionElement.within_group()`메서드를 갖고 있습니다. 987 | 988 | ```python 989 | >>> print( 990 | ... func.unnest( 991 | ... func.percentile_disc([0.25,0.5,0.75,1]).within_group(user_table.c.name) 992 | ... ) 993 | ... ) 994 | """ 995 | unnest(percentile_disc(:percentile_disc_1) WITHIN GROUP (ORDER BY user_account.name)) 996 | """ 997 | ``` 998 | 어떤 백엔드에서는 "FILTER"를 지원하는데 이는 ` FunctionElement.filter()`메서드로 사용이 가능합니다. 999 | ```python 1000 | >>> stmt = select( 1001 | ... func.count(address_table.c.email_address).filter(user_table.c.name == 'sandy'), 1002 | ... func.count(address_table.c.email_address).filter(user_table.c.name == 'spongebob') 1003 | ... ).select_from(user_table).join(address_table) 1004 | >>> with engine.connect() as conn: 1005 | ... result = conn.execute(stmt) 1006 | ... print(result.all()) 1007 | 1008 | """ 1009 | SELECT count(address.email_address) FILTER (WHERE user_account.name = ?) AS anon_1, 1010 | count(address.email_address) FILTER (WHERE user_account.name = ?) AS anon_2 1011 | FROM user_account JOIN address ON user_account.id = address.user_id 1012 | """ 1013 | 1014 | ('sandy', 'spongebob') 1015 | [(2, 1)] 1016 | ``` 1017 | 1018 |
1019 | 1020 | ### Table-Valued Functions 1021 | > 원문의 Table-Valued Functions 부분은 제가 이해하지 못했습니다. 1022 | > 이에 대해 이해하신 분 있으시다면 이 부분에 기여 부탁드립니다. 감사합니다. 1023 | 1024 |
1025 | 1026 | ### 컬럼값 함수 또는 스칼라 컬럼(테이블값 함수) 1027 | Oracle과 PostgresSQL에서 지원하는 특별 문법 중 하나로 FROM절에 세팅되는 함수들이 있습니다. 1028 | PostgreSQL에서는 `json_array_elements()`, `json_object_keys()`, `json_each_text()`, `json_each()`등의 함수가 그 예입니다. 1029 | 1030 | SQLAlchemy는 이러한 함수를 컬럼 값이라고 하며 `Function`객체에 지정자로 `FunctionElement.column_valued()`로 적용하여 사용할 수 있습니다. 1031 | 1032 | ```python 1033 | >>> from sqlalchemy import select, func 1034 | >>> stmt = select(func.json_array_elements('["one", "two"]').column_valued("x")) 1035 | >>> print(stmt) 1036 | """ 1037 | SELECT x 1038 | FROM json_array_elements(:json_array_elements_1) AS x 1039 | """ 1040 | ``` 1041 | 1042 | 컬럼값 함수는 오라클에서도 아래와 같이 커스텀 SQL 함수로 사용할 수 있습니다. 1043 | 1044 | ```python 1045 | >>> from sqlalchemy.dialects import oracle 1046 | >>> stmt = select(func.scalar_strings(5).column_valued("s")) 1047 | >>> print(stmt.compile(dialect=oracle.dialect())) 1048 | """ 1049 | SELECT COLUMN_VALUE s 1050 | FROM TABLE (scalar_strings(:scalar_strings_1)) s 1051 | """ 1052 | ``` 1053 | -------------------------------------------------------------------------------- /src/tutorial/5.2. Core 방식으로 행 삽입하기.md: -------------------------------------------------------------------------------- 1 | # Core 방식으로 행 삽입하기 2 | 3 | 이번 챕터에서는 SQLAlchemy Core 방식으로 데이터를 INSERT 하는 방법을 배웁니다. 4 | 5 |
6 | 7 | ## `insert()` 를 통한 SQL 표현식 구성 8 | 9 | 먼저 다음처럼 INSERT 구문을 만들 수 있습니다. 10 | 11 | ```python 12 | >>> from sqlalchemy import insert 13 | 14 | # stmt는 Insert 객체 인스턴스입니다. 15 | >>> stmt = insert(user_table).values(name='spongebob', fullname="Spongebob Squarepants") 16 | >>> print(stmt) 17 | 'INSERT INTO user_account (name, fullname) VALUES (:name, :fullname)' 18 | ``` 19 | 20 | > 여기서 `user_table`은 우리가 이전 챕터에서 만든 `Table` 객체입니다. 우리는 아래처럼 만들었었습니다. 21 | > 22 | > ```python 23 | > from sqlalchemy import MetaData 24 | > from sqlalchemy import Table, Column, Integer, String 25 | > 26 | > metadata = MetaData() 27 | > user_table = Table( 28 | > 'user_account', 29 | > metadata, 30 | > Column('id', Integer, primary_key=True), 31 | > Column('name', String(30)), 32 | > Column('fullname', String), 33 | > ) 34 | > ``` 35 | 36 | `stmt` 를 보면 아직 매개변수가 매핑되지는 않았습니다. 37 | 이는 다음처럼 `complie()` 한 후에 확인할 수 있습니다. 38 | 39 | ```python 40 | >>> compiled = stmt.compile() 41 | >>> print(compiled.params) 42 | {'name': 'spongebob', 'fullname': 'Spongebob Squarepants'} 43 | ``` 44 | 45 |
46 | 47 | ## 명령문 실행 48 | 49 | 이제 위에서 만든 INSERT 구문을 Core 방식으로 실행해봅시다. 50 | 51 | ```python 52 | >>> with engine.connect() as conn: 53 | ... result = conn.execute(stmt) 54 | ... conn.commit() 55 | 56 | # 위 코드는 결과적으로 아래 쿼리를 실행합니다. 57 | BEGIN (implicit) 58 | INSERT INTO user_account (name, fullname) VALUES (?, ?) 59 | [...] ('spongebob', 'Spongebob Squarepants') 60 | COMMIT 61 | ``` 62 | 63 | `conn.execute(stmt)` 의 반환 값을 받은 `result` 에는 어떤 정보가 있을까요? 64 | `result` 는 [`CursorResult`](https://docs.sqlalchemy.org/en/14/core/connections.html#sqlalchemy.engine.CursorResult) 객체입니다. 65 | 여기에는 실행 결과물에 대한 여러 정보를 담고있는데, 특히 데이터 행을 담고있는 [`Row`](https://docs.sqlalchemy.org/en/14/core/connections.html#sqlalchemy.engine.Row) 객체를 들고있습니다. 66 | 67 | 우리는 방금 데이터를 삽입했고, 이에 대한 결과물로 다음처럼 삽입된 데이터의 기본 키 값을 확인할 수 있습니다. 68 | 69 | ```python 70 | >>> result.inserted_primary_key # 이 역시 Row 객체입니다. 71 | (1, ) # 기본 키가 여러 열로 구성될 수 있으므로 튜플로 표현됩니다. 72 | ``` 73 | 74 |
75 | 76 | ## `Connection.execute()` 에 INSERT 매개변수 전달하기 77 | 78 | 위에서는 다음처럼 `insert` 에 `values` 까지 함께 포함하여 구문울 만들었습니다. 79 | 80 | ```python 81 | >>> stmt = insert(user_table).values(name='spongebob', fullname="Spongebob Squarepants") 82 | ``` 83 | 84 | 하지만 이 방법 외에도 다음처럼 `Connection.execute()` 메서드에 매개변수를 전달하여 INSERT 구문을 실행할 수 있습니다. 85 | 공식문서에는 이게 좀 더 일반적인 방법이라고 말합니다. 86 | 87 | ```python 88 | >>> with engine.connect() as conn: 89 | ... result = conn.execute( 90 | ... insert(user_table), 91 | ... [ 92 | ... {"name": "sandy", "fullname": "Sandy Cheeks"}, 93 | ... {"name": "patrick", "fullname": "Patrick Star"} 94 | ... ] 95 | ... ) 96 | ... conn.commit() 97 | ``` 98 | 99 | > 공식문서에는 하위 쿼리까지 포함하여 실행시키는 법을 별도의 블락에서 설명하고 있는데, 튜토리얼 내용으로는 다소 적합하지 않다고 판단하여 이 글에는 포함하지 않았습니다. 100 | > 이 내용이 궁금하신 분들은 [원문 링크](https://docs.sqlalchemy.org/en/14/tutorial/data_insert.html#insert-usually-generates-the-values-clause-automatically)를 참고하세요. 101 | 102 |
103 | 104 | ## `Insert.from_select()` 105 | 106 | 다음처럼 SELECT 하여 받은 행들을 INSERT 하기 위한 쿼리가 필요한 경우가 있습니다. 107 | 108 | 이런 사례는 예를 들면 다음 코드처럼 작성할 수 있습니다. 109 | 110 | ```python 111 | >>> select_stmt = select(user_table.c.id, user_table.c.name + "@aol.com") 112 | >>> insert_stmt = insert(address_table).from_select( 113 | ... ["user_id", "email_address"], select_stmt 114 | ... ) 115 | >>> print(insert_stmt) 116 | """ 117 | INSERT INTO address (user_id, email_address) 118 | SELECT user_account.id, user_account.name || :name_1 AS anon_1 119 | FROM user_account 120 | """ 121 | ``` 122 | 123 |
124 | 125 | ## `Insert.returning()` 126 | 127 | 데이터베이스에서 쿼리 처리 후에 처리된 행의 값을 반환받아야 하는 경우가 있습니다. 이를 `RETURNING` 문법이라 합니다. 128 | 이에 대한 소개 글은 [이 블로그 글](https://blog.gaerae.com/2015/10/postgresql-insert-update-returning.html)을 읽어보시면 좋을거 같습니다. 129 | 130 | SQLAlchemy Core에서는 이런 `RETURNING` 문법을 다음처럼 작성할 수 있습니다. 131 | 132 | ```python 133 | >>> insert_stmt = insert(address_table).returning(address_table.c.id, address_table.c.email_address) 134 | >>> print(insert_stmt) 135 | """ 136 | INSERT INTO address (id, user_id, email_address) 137 | VALUES (:id, :user_id, :email_address) 138 | RETURNING address.id, address.email_address 139 | """ 140 | ``` 141 | -------------------------------------------------------------------------------- /src/tutorial/5.3. Core 방식으로 행 수정 및 삭제하기.md: -------------------------------------------------------------------------------- 1 | # Core 방식으로 행 수정 및 삭제하기 2 | 3 | 이번 챕터에서는 Core 방식으로 기존 행을 수정하고 삭제하는 데 사용되는 Update 및 Delete 구문에 대해 설명합니다. 4 | 5 |
6 | 7 | ## `update()` 를 통한 SQL 표현식 구성 8 | 9 | 다음처럼 UPDATE 구문을 작성할 수 있습니다. 10 | 11 | ```python 12 | >>> from sqlalchemy import update 13 | >>> stmt = ( 14 | ... update(user_table).where(user_table.c.name == 'patrick'). 15 | ... values(fullname='Patrick the Star') 16 | ... ) 17 | >>> print(stmt) 18 | 'UPDATE user_account SET fullname=:fullname WHERE user_account.name = :name_1' 19 | ``` 20 | 21 | ```python 22 | >>> stmt = ( 23 | ... update(user_table). 24 | ... values(fullname="Username: " + user_table.c.name) 25 | ... ) 26 | >>> print(stmt) 27 | 'UPDATE user_account SET fullname=(:name_1 || user_account.name)' 28 | ``` 29 | 30 | > 원문에는 `bindparam()` 에 대한 내용이 나오는데, 사용 사례를 잘 본 적이 없어서 이 글에서는 생략합니다. 궁금하신 분은 [원문 내용](https://docs.sqlalchemy.org/en/14/tutorial/data_update.html)을 참고하세요. 31 | 32 |
33 | 34 | ### Correlated 업데이트 35 | 36 | 다음처럼 [Correlated Subquery](https://docs.sqlalchemy.org/en/14/tutorial/data_select.html#tutorial-scalar-subquery)를 사용하여 다른 테이블의 행을 사용할 수 있습니다. 37 | 38 | ```python 39 | >>> scalar_subq = ( 40 | ... select(address_table.c.email_address). 41 | ... where(address_table.c.user_id == user_table.c.id). 42 | ... order_by(address_table.c.id). 43 | ... limit(1). 44 | ... scalar_subquery() 45 | ... ) 46 | >>> update_stmt = update(user_table).values(fullname=scalar_subq) 47 | >>> print(update_stmt) 48 | """ 49 | UPDATE user_account SET fullname=(SELECT address.email_address 50 | FROM address 51 | WHERE address.user_id = user_account.id ORDER BY address.id 52 | LIMIT :param_1) 53 | """ 54 | ``` 55 | 56 |
57 | 58 | ### 다른 테이블과 연관된 조건으로 업데이트 59 | 60 | 테이블을 업데이트할 때, 다른 테이블의 정보와 연관하여 조건을 설정해야할 때가 있습니다. 61 | 이 경우, 예를들면 다음처럼 사용할 수 있습니다. 62 | 63 | ```python 64 | >>> update_stmt = ( 65 | ... update(user_table). 66 | ... where(user_table.c.id == address_table.c.user_id). 67 | ... where(address_table.c.email_address == 'patrick@aol.com'). 68 | ... values(fullname='Pat') 69 | ... ) 70 | >>> print(update_stmt) 71 | """ 72 | UPDATE user_account SET fullname=:fullname FROM address 73 | WHERE user_account.id = address.user_id AND address.email_address = :email_address_1 74 | """ 75 | ``` 76 | 77 |
78 | 79 | ### 여러 테이블 동시에 업데이트 80 | 81 | 다음처럼 여러 테이블에서 조건에 해당하는 특정 값들을 동시에 업데이트할 수 있습니다. 82 | 83 | ```python 84 | >>> update_stmt = ( 85 | ... update(user_table). 86 | ... where(user_table.c.id == address_table.c.user_id). 87 | ... where(address_table.c.email_address == 'patrick@aol.com'). 88 | ... values( 89 | ... { 90 | ... user_table.c.fullname: "Pat", 91 | ... address_table.c.email_address: "pat@aol.com" 92 | ... } 93 | ... ) 94 | ... ) 95 | >>> from sqlalchemy.dialects import mysql 96 | >>> print(update_stmt.compile(dialect=mysql.dialect())) 97 | """ 98 | UPDATE user_account, address 99 | SET address.email_address=%s, user_account.fullname=%s 100 | WHERE user_account.id = address.user_id AND address.email_address = %s 101 | """ 102 | ``` 103 | 104 | > 원문의 Parameter Ordered Updates 부분은 제가 이해하지 못하여 정리하지 않았습니다. 105 | > 잘 아시는 분이 있으면 이 문서에 기여해주시면 감사하겠습니다. 106 | 107 |
108 | 109 | ## `delete()` 를 통한 SQL 표현식 구성 110 | 111 | 다음처럼 DELETE 구문을 작성할 수 있습니다. 112 | 113 | ```python 114 | >>> from sqlalchemy import delete 115 | >>> stmt = delete(user_table).where(user_table.c.name == 'patrick') 116 | >>> print(stmt) 117 | """ 118 | DELETE FROM user_account WHERE user_account.name = :name_1 119 | """ 120 | ``` 121 | 122 |
123 | 124 | ### 다른 테이블과 조인하여 삭제 125 | 126 | 다른 테이블과 조인한 뒤, 특정 조건에 맞는 데이터만 삭제해야 하는 경우가 있습니다. (이해가 안간다면 [이 글](https://servedev.tistory.com/61)을 참고해보세요.) 127 | 이 경우, 예를들면 다음처럼 사용할 수 있습니다. 128 | 129 | ```python 130 | >>> delete_stmt = ( 131 | ... delete(user_table). 132 | ... where(user_table.c.id == address_table.c.user_id). 133 | ... where(address_table.c.email_address == 'patrick@aol.com') 134 | ... ) 135 | >>> from sqlalchemy.dialects import mysql 136 | >>> print(delete_stmt.compile(dialect=mysql.dialect())) 137 | """ 138 | DELETE FROM user_account USING user_account, address 139 | WHERE user_account.id = address.user_id AND address.email_address = %s 140 | """ 141 | ``` 142 | 143 |
144 | 145 | ## UPDATE, DELETE에서 영향을 받는 행 수 얻기 146 | 147 | 다음처럼 [`Result.rowcount` 속성](https://docs.sqlalchemy.org/en/14/core/connections.html#sqlalchemy.engine.CursorResult.rowcount)을 통해 쿼리가 처리한 행 수를 가져올 수 있습니다. 148 | 149 | ```python 150 | >>> with engine.begin() as conn: 151 | ... result = conn.execute( 152 | ... update(user_table). 153 | ... values(fullname="Patrick McStar"). 154 | ... where(user_table.c.name == 'patrick') 155 | ... ) 156 | ... print(result.rowcount) # Result 객체의 rowcount 속성을 사용합니다. 157 | 158 | 1 # 쿼리가 처리한 행 수 (조건절에 걸리는 행 수와 같습니다.) 159 | ``` 160 | 161 |
162 | 163 | ## UPDATE, DELETE와 함께 RETURNING 사용하기 164 | 165 | 다음처럼 RETURNING 문법을 사용할 수 있습니다. (RETURNING 문법에 대해서는 [이 글](https://blog.gaerae.com/2015/10/postgresql-insert-update-returning.html)을 참고해보세요.) 166 | 167 | ```python 168 | >>> update_stmt = ( 169 | ... update(user_table).where(user_table.c.name == 'patrick'). 170 | ... values(fullname='Patrick the Star'). 171 | ... returning(user_table.c.id, user_table.c.name) 172 | ... ) 173 | >>> print(update_stmt) 174 | """ 175 | UPDATE user_account SET fullname=:fullname 176 | WHERE user_account.name = :name_1 177 | RETURNING user_account.id, user_account.name 178 | """ 179 | ``` 180 | 181 | ```python 182 | >>> delete_stmt = ( 183 | ... delete(user_table).where(user_table.c.name == 'patrick'). 184 | ... returning(user_table.c.id, user_table.c.name) 185 | ... ) 186 | >>> print(delete_stmt) 187 | """ 188 | DELETE FROM user_account 189 | WHERE user_account.name = :name_1 190 | RETURNING user_account.id, user_account.name 191 | """ 192 | ``` -------------------------------------------------------------------------------- /src/tutorial/6. ORM 방식으로 데이터 조작하기.md: -------------------------------------------------------------------------------- 1 | # ORM 방식으로 데이터 조작하기 2 | 3 | 이전 챕터까지 CORE 관점에서 쿼리를 활용하는 방식에 초점을 맞췄습니다. 이번 챕터에서는 ORM 방식에서 쓰이는 `Session`의 구성 요소와 수명 주기, 상호 작용하는 방법을 설명합니다. 4 | 5 |
6 | 7 | ## ORM으로 행 삽입하기 8 | 9 | `Session` 객체는 ORM을 사용할 때 `Insert` 객체들을 만들고 트랜잭션에서 이 객체들을 내보내는 역할을 합니다. 10 | `Session`은 이러한 과정들을 수행하기 위해 객체 항목을 추가합니다. 11 | 그 후 flush라는 프로세스를 통해 새로운 항목들을 데이터베이스에 기록합니다. 12 | 13 | ### 행을 나타내는 객체의 인스턴스 14 | 15 | 이전 과정에서 우리는 `Python Dict`를 사용하여 `INSERT`를 실행하였습니다. 16 | 17 | ORM에서는 테이블 메타데이터 정의에서 정의한 사용자 정의 Python 객체를 직접 사용합니다. 18 | 19 | ```python 20 | >>> squidward = User(name="squidward", fullname="Squidward Tentacles") 21 | >>> krabs = User(name="ehkrabs", fullname="Eugene H. Krabs") 22 | ``` 23 | 24 | `INSERT` 될 잠재적인 데이터베이스 행을 나타내는 두 개의 `User` 객체를 만듭니다. 25 | ORM 매핑에 의해 자동으로 생성된 `__init__()` 생성자 덕에 생성자의 열 이름을 키로 사용하여 각 객체를 생성할 수 있습니다. 26 | 27 | ```python 28 | >>> squidward 29 | User(id=None, name='squidward', fullname='Squidward Tentacles') 30 | ``` 31 | 32 | Core의 `Insert`와 유사하게, 기본 키를 포함하지 않아도 ORM이 이를 통합시켜 줍니다. 33 | `id`의 `None` 값은 속성에 아직 값이 없음을 나타내기 위해 SQLAlchemy에서 제공합니다. 34 | 35 | 현재 위의 두 객체(`squiward`와 `krabs`)는 `transient` 상태라고 불리게 됩니다. 36 | `transient` 상태란, 어떤 데이터베이스와 연결되지 않고, `INSERT`문을 생성할 수 있는 `Session `객체와도 아직 연결되지 않은 상태를 의미합니다. 37 | 38 | ### `Session`에 객체 추가하기 39 | 40 | ```python 41 | >>> session = Session(engine) # 반드시 사용 후 close 해야 합니다. 42 | >>> session.add(squidward) # Session.add() 매소드를 통해서 객체를 Session에 추가해줍니다. 43 | >>> session.add(krabs) 44 | ``` 45 | 46 | 객체가 `Session.add()`를 통해서 `Session`에 추가하게 되면, `pending` 상태가 되었다고 부릅니다. 47 | `pending` 상태는 아직 데이터베이스에 추가되지 않은 상태입니다. 48 | 49 | ```python 50 | >>> session.new # session.new를 통해서 pending 상태에 있는 객체들을 확인할 수 있습니다. 51 | IdentitySet([User(id=None, name='squidward', fullname='Squidward Tentacles'), User(id=None, name='ehkrabs', fullname='Eugene H. Krabs')]) 52 | ``` 53 | 54 | - `IdentitySet`은 모든 경우에 객체 ID를 hash하는 Python `set`입니다. 55 | - 즉, Python 내장 함수 중 `hash()`가 아닌, `id()` 메소드를 사용하고 있습니다. 56 | 57 | ### Flushing 58 | 59 | `Session` 객체는 [`unit of work` 패턴](https://zetlos.tistory.com/1179902868)을 사용합니다. 이는 변경 사항을 누적하지만, 필요할 때까지는 실제로 데이터베이스와 통신을 하지 않음을 의미합니다. 60 | 이런 동작 방식을 통해서 위에서 언급한 `pending` 상태의 객체들이 더 효율적인 SQL DML로 사용됩니다. 61 | 현재의 변경된 사항들을 실제로 Database에 SQL을 통해 내보내는 작업을 flush 이라고 합니다. 62 | 63 | ```python 64 | >>> session.flush() 65 | """ 66 | INSERT INTO user_account (name, fullname) VALUES (?, ?) 67 | [...] ('squidward', 'Squidward Tentacles') 68 | INSERT INTO user_account (name, fullname) VALUES (?, ?) 69 | [...] ('ehkrabs', 'Eugene H. Krabs') 70 | """ 71 | ``` 72 | 73 | 이제 트랜잭션은 `Session.commit()`, `Session.rollback()`, `Session.close()` 중 하나가 호출될 때 까지 열린 상태로 유지됩니다. 74 | 75 | `Session.flush()`를 직접 사용하여, 현재 `pending` 상태에 있는 내용을 직접 밀어넣을 수 있지만, Session은 autoflush라는 동작을 특징으로 하므로 일반적으로는 필요하지 않습니다. `Session.commit()`이 호출 될 때 마다 변경 사항을 flush 합니다. 76 | 77 | ### 자동 생성된 기본 키 속성 78 | 79 | 행이 삽입되게 되면, 우리가 생성한 Python 객체는 `persistent` 라는 상태가 됩니다. 80 | `persistent` 상태는 로드된 `Session` 객체와 연결됩니다. 81 | 82 | `INSERT` 실행 시, ORM이 각각의 새 객체에 대한 기본 키 식별자를 검색하는 효과를 가져옵니다. 83 | 이전에 소개한것과 동일한 `CursorResult.inserted_primary_key` 접근자를 사용합니다. 84 | 85 | ```python 86 | >>> squidward.id 87 | 4 88 | >>> krabs.id 89 | 5 90 | ``` 91 | 92 | > ORM이 flush 될 때, `executemany` 대신, 두 개의 다른 INSERT 문을 사용하는 이유가 바로 이 `CursorResult.inserted_primary_key` 때문입니다. 93 | > - SQLite의 경우 한 번에 한 열을 `INSERT` 해야 자동 증가 기능을 사용할 수 있습니다.(PostgreSQL의 IDENTITY나 SERIAL 기능등 다른 다양한 데이터베이스들의 경우들도 이처럼 동작합니다.) 94 | > - `psycopg2`와 같이 한번에 많은 데이터에 대한 기본 키 정보를 제공 받을 수 있는 데이터베이스가 연결되어 있다면, ORM은 이를 최적화하여 많은 열을 한번에 `INSERT` 하도록 합니다. 95 | 96 | ### Identity Map 97 | 98 | `Identity Map`(`ID Map`)은 현재 메모리에 로드된 모든 객체를 기본 키 ID에 연결하는 메모리 내 저장소입니다. 99 | `Session.get()`을 통해서 객체 중 하나를 검색할 수 있습니다. 100 | 이 메소드는 객체가 메모리에 있으면, `ID Map`에서, 그렇지 않으면 `SELECT`문을 통해서 객체를 검색합니다. 101 | 102 | ```python 103 | >>> some_squidward = session.get(User, 4) 104 | >>> some_squidward 105 | User(id=4, name='squidward', fullname='Squidward Tentacles') 106 | ``` 107 | 108 | 중요한 점은, `ID Map`은 Python 객체 중에서도 고유한 객체를 유지하고 있다는 점입니다. 109 | 110 | ```python 111 | >>> some_squidward is squidward 112 | True 113 | ``` 114 | 115 | `ID map`은 동기화되지 않은 상태에서, 트랜잭션 내에서 복잡한 개체 집합을 조작할 수 있도록 하는 중요한 기능입니다. 116 | 117 | ### Committing 118 | 119 | 현재까지의 변경사항을 트랜잭션에 `commit` 합니다. 120 | 121 | ```python 122 | >>> session.commit() 123 | COMMIT 124 | ``` 125 | 126 |
127 | 128 | ## ORM 객체 `UPDATE`하기 129 | 130 | ORM을 통해 `UPDATE` 하는 방법에는 2가지 방법이 있습니다. 131 | 132 | 1. `Session`에서 사용하는 `unit of work` 패턴 방식이 있습니다. 변경사항이 있는 기본 키 별로 `UPDATE` 작업이 순서대로 내보내지게 됩니다. 133 | 2. "ORM 사용 업데이트"라고 하며 명시적으로 Session과 함께 `Update` 구성을 사용할 수도 있습니다. 134 | 135 | ### 변경사항을 자동으로 업데이트하기 136 | 137 | ```python 138 | >>> sandy = session.execute(select(User).filter_by(name="sandy")).scalar_one() 139 | """ 140 | SELECT user_account.id, user_account.name, user_account.fullname 141 | FROM user_account 142 | WHERE user_account.name = ? 143 | [...] ('sandy',) 144 | """ 145 | ``` 146 | 147 | 이 'Sandy' 유저 객체는 데이터베이스에서 행, 더 구체적으로는 트랙잭션 측면에서 기본 키가 2인 행에 대한 `proxy` 역할을 합니다. 148 | 149 | ```python 150 | >>> sandy 151 | User(id=2, name='sandy', fullname='Sandy Cheeks') 152 | >>> sandy.fullname = "Sandy Squirrel" # 객체의 속성을 변화시키면, Session은 이 변화를 기록합니다. 153 | >>> sandy in session.dirty # 이렇게 변한 객체는 dirty 라고 불리우며 session.dirty에서 확인 할 수 있습니다. 154 | True 155 | ``` 156 | 157 | Session이 `flush`를 실행하게 되면, 데이터베이스에서 `UPDATE`가 실행되어 데이터베이스에 실제로 값을 갱신합니다. `SELECT` 문을 추가로 실행하게 되면, 자동으로 `flush`가 실행되어 sandy의 바뀐 이름 값을 `SELECT`를 통해서 바로 얻을 수 있습니다. 158 | 159 | ```python 160 | >>> sandy_fullname = session.execute( 161 | ... select(User.fullname).where(User.id == 2) 162 | ... ).scalar_one() 163 | """ 164 | UPDATE user_account SET fullname=? WHERE user_account.id = ? 165 | [...] ('Sandy Squirrel', 2) 166 | SELECT user_account.fullname 167 | FROM user_account 168 | WHERE user_account.id = ? 169 | [...] (2,) 170 | """ 171 | >>> print(sandy_fullname) 172 | Sandy Squirrel 173 | # flush를 통해 sandy의 변화가 실제로 데이터베이스에 반영되어, dirty 속성을 잃게 됩니다. 174 | >>> sandy in session.dirty 175 | False 176 | ``` 177 | 178 | ### ORM 사용 업데이트 179 | 180 | ORM을 통해 `UPDATE` 하는 마지막 방법으로 `ORM 사용 업데이트`를 명시적으로 사용하는 방법이 있습니다. 이를 사용하면 한 번에 많은 행에 영향을 줄 수 있는 일반 SQL `UPDATE` 문을 사용할 수 있습니다. 181 | 182 | ```python 183 | >>> session.execute( 184 | ... update(User). 185 | ... where(User.name == "sandy"). 186 | ... values(fullname="Sandy Squirrel Extraordinaire") 187 | ... ) 188 | """ 189 | UPDATE user_account SET fullname=? WHERE user_account.name = ? 190 | [...] ('Sandy Squirrel Extraordinaire', 'sandy') 191 | """ 192 | 193 | ``` 194 | 195 | 현재 `Session`에서 주어진 조건과 일치하는 객체가 있다면, 이 객체에도 해당하는 `update`가 반영되게 됩니다. 196 | ```python 197 | >>> sandy.fullname 198 | 'Sandy Squirrel Extraordinaire' 199 | ``` 200 | 201 |
202 | 203 | ## ORM 객체를 삭제하기 204 | 205 | `Session.delete()` 메서드를 사용하여 개별 ORM 객체를 삭제 대상으로 표시할 수 있습니다. `delete`가 수행되면, 해당 `Session`에 존재하는 객체들은 `expired` 상태가 되게 됩니다. 206 | 207 | ```python 208 | >>> patrick = session.get(User, 3) 209 | """ 210 | SELECT user_account.id AS user_account_id, user_account.name AS user_account_name, 211 | user_account.fullname AS user_account_fullname 212 | FROM user_account 213 | WHERE user_account.id = ? 214 | [...] (3,) 215 | """ 216 | >>> session.delete(patrick) # patrik을 삭제 할 것이라고 명시 217 | >>> session.execute(select(User).where(User.name == "patrick")).first() # 이 시점에서 flush 실행 218 | """ 219 | SELECT address.id AS address_id, address.email_address AS address_email_address, 220 | address.user_id AS address_user_id 221 | FROM address 222 | WHERE ? = address.user_id 223 | [...] (3,) 224 | DELETE FROM user_account WHERE user_account.id = ? 225 | [...] (3,) 226 | SELECT user_account.id, user_account.name, user_account.fullname 227 | FROM user_account 228 | WHERE user_account.name = ? 229 | [...] ('patrick',) 230 | """ 231 | >>> squidward in session # Session에서 만료되면, 해당 객체는 session에서 삭제됩니다. 232 | False 233 | ``` 234 | 235 | 위의 `UPDATE`에서 사용된 'Sandy'와 마찬가지로, 해당 작업들은 진행중인 트랜잭션에서만 이루어진 일이며 `commit` 하지 않는 이상, 언제든 취소할 수 있습니다. 236 | 237 | ### ORM 사용 삭제하기 238 | 239 | `UPDATE`와 마찬가지로 `ORM 사용 삭제하기`도 있습니다. 240 | 241 | ```python 242 | # 예시를 위한 작업일 뿐, 실제로 delete에서 필요한 작업은 아닙니다. 243 | >>> squidward = session.get(User, 4) 244 | """ 245 | SELECT user_account.id AS user_account_id, user_account.name AS user_account_name, 246 | user_account.fullname AS user_account_fullname 247 | FROM user_account 248 | WHERE user_account.id = ? 249 | [...] (4,) 250 | """ 251 | 252 | >>> session.execute(delete(User).where(User.name == "squidward")) 253 | """ 254 | DELETE FROM user_account WHERE user_account.name = ? 255 | [...] ('squidward',) 256 | 257 | """ 258 | ``` 259 | 260 |
261 | 262 | ## Rolling Back 263 | 264 | `Session`에는 현재의 작업들을 롤백하는 `Session.rollback()` 메소드가 존재합니다. 이 메소드는 위에서 사용된 `sandy`와 같은 Python 객체에도 영향을 미칩니다. 265 | `Session.rollback()`을 호출하면 트랜잭션을 롤백할 뿐만 아니라 현재 이 `Session`과 연결된 모든 객체를 `expired` 상태로 바꿉니다. 이러한 상태 변경은 다음에 객체에 접근 할 때 스스로 새로 고침을 하는 효과가 있고 이러한 프로세스를 `지연 로딩` 이라고 합니다. 266 | 267 | ```python 268 | >>> session.rollback() 269 | ROLLBACK 270 | ``` 271 | 272 | `expired` 상태의 객체인 `sandy` 를 자세히 보면, 특별한 SQLAlchemy 관련 상태 객체를 제외하고 다른 정보가 남아 있지 않음을 볼 수 있습니다. 273 | 274 | ```python 275 | >>> sandy.__dict__ 276 | {'_sa_instance_state': } 277 | >>> sandy.fullname # session이 만료되었으므로, 해당 객체 속성에 접근 시, 트랜잭션이 새로 일어납니다. 278 | """ 279 | SELECT user_account.id AS user_account_id, user_account.name AS user_account_name, 280 | user_account.fullname AS user_account_fullname 281 | FROM user_account 282 | WHERE user_account.id = ? 283 | [...] (2,) 284 | """ 285 | 'Sandy Cheeks' 286 | >>> sandy.__dict__ #이제 데이터베이스 행이 sandy 객체에도 채워진 것을 볼 수 있습니다. 287 | {'_sa_instance_state': , 288 | 'id': 2, 'name': 'sandy', 'fullname': 'Sandy Cheeks'} 289 | ``` 290 | 291 | 삭제된 객체에 대해서도, `Session`에 다시 복원되었으며 데이터베이스에도 다시 나타나는 걸 볼 수 있습니다. 292 | ```python 293 | >>> patrick in session 294 | True 295 | >>> session.execute(select(User).where(User.name == 'patrick')).scalar_one() is patrick 296 | """ 297 | SELECT user_account.id, user_account.name, user_account.fullname 298 | FROM user_account 299 | WHERE user_account.name = ? 300 | [...] ('patrick',) 301 | """ 302 | True 303 | ``` 304 | 305 |
306 | 307 | ## `Session` 종료하기 308 | 309 | 우리는 컨텍스트 구문 외부에서 `Session`을 다뤘는데, 이런 경우 다음처럼 명시적으로 `Session`을 닫아주는 것이 좋습니다. 310 | 311 | ```python 312 | >>> session.close() 313 | ROLLBACK 314 | ``` 315 | 316 | 마찬가지로 컨텍스트 구문을 통해 생성한 `Session`을 컨텍스트 구문 내에서 닫으면 다음 작업들이 수행됩니다. 317 | 318 | - 진행 중인 모든 트랜잭션을 취소(예: 롤백)하여 연결 풀에 대한 모든 연결 리소스를 해제합니다. 319 | - 즉, `Session`을 사용하여 일부 읽기 전용 작업을 수행한 다음, 닫을 때 트랜잭션이 롤백되었는지 확인하기 위해 `Session.rollback()`을 명시적으로 호출할 필요가 없습니다. 연결 풀이 이를 처리합니다. 320 | - `Session`에서 모든 개체를 삭제합니다. 321 | - 이것은 sandy, patrick 및 squidward와 같이 이 `Session`에 대해 로드한 모든 Python 개체가 이제 `detached` 상태에 있음을 의미합니다. 예를 들어 `expired` 상태에 있던 객체는 `Session.commit()` 호출로 인해 현재 행의 상태를 포함하지 않고 새로 고칠 데이터베이스 트랜잭션과 더 이상 연관되지 않습니다. 322 | - ```python 323 | >>> squidward.name 324 | Traceback (most recent call last): 325 | ... 326 | sqlalchemy.orm.exc.DetachedInstanceError: Instance is not bound to a Session; attribute refresh operation cannot proceed 327 | ``` 328 | - `detached`된 객체는 `Session.add()` 메서드를 사용하여 동일한 객체 또는 새 `Session`과 다시 연결될 수 있습니다. 그러면 특정 데이터베이스 행과의 관계가 다시 설정됩니다. 329 | - ```python 330 | >>> session.add(squidward) # session에 다시 연결 331 | >>> squidward.name # 트랜잭션을 통해 정보를 다시 불러옵니다. 332 | """ 333 | SELECT user_account.id AS user_account_id, user_account.name AS user_account_name, user_account.fullname AS user_account_fullname 334 | FROM user_account 335 | WHERE user_account.id = ? 336 | [...] (4,) 337 | """ 338 | 'squidward' 339 | ``` 340 | 341 | > `detached` 상태의 개체는 되도록이면 사용을 지양해야 합니다. `Session`이 닫히면 이전에 연결된 모든 개체에 대한 참조도 정리합니다. 일반적으로 `detached`된 객체가 필요한 경우는 웹 어플리케이션에서 방금 커밋된 개체를 뷰에서 렌더링되기 전에 `Session`이 닫힌 경우가 있습니다. 이 경우 `Session.expire_on_commit` 플래그를 False로 설정합니다. 342 | 343 | 344 | -------------------------------------------------------------------------------- /src/tutorial/7. ORM 방식으로 관련 개체 작업하기.md: -------------------------------------------------------------------------------- 1 | # ORM으로 관련 개체 작업하기 2 | 3 |
4 | 5 | 이번 챕터에서는 다른 객체를 참조하는 매핑된 객체와 상호작용하는 방식인 또 하나의 필수적인 ORM 개념을 다룰 것입니다. 6 | `relationship()`은 매핑된 두 객체 간의 관계를 정의하며, **자기 참조**관계라고도 합니다. 7 | 기본적인 구조를 위해 `Column` 매핑 및 기타 지시문을 생략하고 짧은 형식으로 `relationship()`을 설명드리겠습니다. 8 | 9 |
10 | 11 | ```python 12 | from sqlalchemy.orm import relationship 13 | 14 | 15 | class User(Base): 16 | __tablename__ = 'user_account' 17 | 18 | # ... Column mappings 19 | 20 | addresses = relationship("Address", back_populates="user") 21 | 22 | 23 | class Address(Base): 24 | __tablename__ = 'address' 25 | 26 | # ... Column mappings 27 | 28 | user = relationship("User", back_populates="addresses") 29 | ``` 30 | 31 |
32 | 33 | 위 구조를 보면 `User` 객체에는 `addresses` 변수, `Address` 객체에는 `user` 라는 변수가 있습니다. 34 | 공통적으로 `relationship` 객체로 생성되어져 있는 것을 볼 수 있습니다. 35 | 이는 실제 **데이터베이스에 컬럼**으로 존재하는 변수는 아니지만 코드 상에서 쉽게 접근할 수 있도록 하기 위해 설정 되었습니다. 36 | 즉, `User` 객체에서 `Address` 객체로 쉽게 찾아갈 수 있게 해줍니다. 37 | 38 | 또한 `relationship` 선언시 파라미터로 `back_populates` 항목은 반대의 상황 즉, 39 | `Address` 객체에서 `User` 객체를 찾아 갈 수 있게 해줍니다. 40 | 41 | > 관계형으로 보았을 경우 1 : N 관계를 자연스럽게 N : 1 관계로 해주는 설정입니다. 42 | 43 | 다음 섹션에서 `relationship()` 객체의 인스턴스가 어떤 역할을 하는지, 동작하는지 보겠습니다. 44 | 45 |
46 | 47 | ## 관계된 객체 사용하기 48 | 49 |
50 | 51 | 새로운 `User` 객체를 만들면 `.addresses` 컬렉션이 나타나는데 `List` 객체임을 알 수 있습니다. 52 | 53 | ```python 54 | >>> u1 = User(name='pkrabs', fullname='Pearl Krabs') 55 | >>> u1.addresses 56 | [] 57 | ``` 58 | 59 | `list.append()`를 사용하여 `Address` 객체를 추가할 수 있습니다. 60 | 61 | ```python 62 | >>> a1 = Address(email_address="pear1.krabs@gmail.com") 63 | >>> u1.addresses.append(a1) 64 | 65 | # u1.addresses 컬렉션에 새로운 Address 객체가 포함되었습니다. 66 | >>> u1.addresses 67 | [Address(id=None, email_address='pearl.krabs@gmail.com')] 68 | ``` 69 | 70 | `Address` 객체를 인스턴스 `User.addresses` 컬렉션과 연관시켰다면 변수 `u1` 에는 또 다른 동작이 발생하는데, 71 | `User.addresses` 와 `Address.user` 관계가 동기화 되어 72 | - `User` 객체에서 `Address` 이동할 수 있을 뿐만 아니라 73 | - `Address` 객체에서 다시 `User` 객체로 이동할 수도 있습니다. 74 | 75 | ```python 76 | >>> a1.user 77 | User(id=None, name='pkrabs', fullname='Pearl Krabs') 78 | ``` 79 | 80 | 두개의 `relationshiop()` 객체 간의 `relationship.back_populates` 을 사용한 동기화 결과입니다. 81 | 82 | 매개변수 `relationshiop()` 는 보완적으로 할당/목록 변형이 발생할때 다른 변수로 지정할 수 있습니다. 83 | 다른 `Address` 객체를 생성하고 해당 `Address.user` 속성에 할당하면 해당 객체 `Address`에 대한 `User.addresses` 컬렉션의 일부가 되는것도 확인 할 수 있습니다. 84 | 85 | ```python 86 | >>> a2 = Address(email_address="pearl@aol.com", user=u1) 87 | >>> u1.addresses 88 | [Address(id=None, email_address='pearl.krabs@gmail.com'), Address(id=None, email_address='pearl@aol.com')] 89 | ``` 90 | 91 |
92 | 93 | 우리는 실제로 객체(`Address`)에 선언된 속성처럼 `user`의 키워드 인수로 `u1` 변수를 사용했습니다. 94 | 다음 사실 이후에 속성을 할당하는 것과 같습니다. 95 | 96 | ```python 97 | # equivalent effect as a2 = Address(user=u1) 98 | >>> a2.user = u1 99 | ``` 100 | 101 |
102 | 103 | ## `Session`에 객체 캐스케이딩 104 | 105 |
106 | 107 | 이제 메모리의 양방향 구조와 연결된 두 개의 `User`, `Address` 객체가 있지만 이전에 [ORM으로 행 삽입하기] 에서 언급했듯이 이러한 객체는 객체와 연결될 때까지 [일시적인] `Session` 상태에 있습니다. 108 | 109 | 우리는 `Session.add()` 를 사용하고, `User` 객체에 메서드를 적용할 때 관련 `Address` 객체도 추가된다는 점을 확인해 볼 필요가 있습니다. 110 | 111 | ```python 112 | >>> session.add(u1) 113 | >>> u1 in session 114 | True 115 | >>> a1 in session 116 | True 117 | >>> a2 in session 118 | True 119 | ``` 120 | 121 | 세 개의 객체는 이제 [보류] 상태에 있으며, 이는 INSERT 작업이 진행되지 않았음을 의미합니다. 122 | 세 객체는 모두 기본 키가 할당되지 않았으며, 또한 a1 및 a2 객체에는 열(`user_id`)을 참조 속성이 있습니다. 123 | 이는 객체가 아직 실제 데이터베이스 연결되지 않았기 때문입니다. 124 | 125 | ```python 126 | >>> print(u1.id) 127 | None 128 | >>> print(a1.user_id) 129 | None 130 | ``` 131 | 132 | 데이터베이스에 저장해봅시다. 133 | 134 | ```python 135 | >>> session.commit() 136 | ``` 137 | 138 | 구현한 코드를 SQL 쿼리로 동작을 해본다면 이와 같습니다. 139 | 140 | ```sql 141 | INSERT INTO user_account (name, fullname) VALUES (?, ?) 142 | [...] ('pkrabs', 'Pearl Krabs') 143 | INSERT INTO address (email_address, user_id) VALUES (?, ?) 144 | [...] ('pearl.krabs@gmail.com', 6) 145 | INSERT INTO address (email_address, user_id) VALUES (?, ?) 146 | [...] ('pearl@aol.com', 6) 147 | COMMIT 148 | ``` 149 | 150 | `session`을 사용하여 SQL의 INSERT, UPDATE, DELETE 문을 자동화할 수 있습니다. 151 | 마지막으로 `Session.commit()`을 실행하여 모든 단계를 올바른 순서로 호출되며 `user_account`에 `address.user_id` 기본키가 적용됩니다. 152 | 153 |
154 | 155 | ## 관계 로드 156 | 157 |
158 | 159 | `Session.commit()` 을 호출한 이후에는 `u1` 객체에 생성된 기본 키를 볼 수 있게됩니다. 160 | 161 | ```python 162 | >>> u1.id 163 | 6 164 | ``` 165 | 166 | > 위 코드는 다음 쿼리를 실행하는 것과 같습니다. 167 | 168 | ```sql 169 | BEGIN (implicit) 170 | SELECT user_account.id AS user_account_id, user_account.name AS user_account_name, 171 | user_account.fullname AS user_account_fullname 172 | FROM user_account 173 | WHERE user_account.id = ? 174 | [...] (6,) 175 | ``` 176 | 177 | 다음처럼 `u1.addresses` 에 연결된 객체들에도 `id`가 들어와있는 것을 볼 수 있습니다. 178 | 해당 객체를 검색하기 위해 우리는 **lazy load** 방식으로 볼 수 있습니다. 179 | 180 | > lazy loading : 누군가 해당 정보에 접근하고자 할때 그때 SELECT문을 날려서 정보를 충당하는 방식. 즉, 그때그때 필요한 정보만 가져오는 것입니다. 181 | 182 | ```python 183 | >>> u1.addresses 184 | [Address(id=4, email_address='pearl.krabs@gmail.com'), Address(id=5, email_address='pearl@aol.com')] 185 | ``` 186 | 187 | > 위 코드는 다음 쿼리를 실행하는 것과 같습니다. 188 | > 189 | ```sql 190 | SELECT address.id AS address_id, address.email_address AS address_email_address, 191 | address.user_id AS address_user_id 192 | FROM address 193 | WHERE ? = address.user_id 194 | [...] (6,) 195 | ``` 196 | 197 | SQLAlchemy ORM의 기본 컬렉션 및 관련 특성은 **lazy loading** 입니다. 즉, 한번 `relationship` 된 컬렉션은 데이터가 메모리에 존재하는 한 계속 접근을 사용할 수 있습니다. 198 | 199 | ```python 200 | >>> u1.addresses 201 | [Address(id=4, email_address='pearl.krabs@gmail.com'), Address(id=5, email_address='pearl@aol.com')] 202 | ``` 203 | lazy loading은 최적화를 위한 명시적인 단계를 수행하지 않으면 비용이 많이 들 수 있지만, 적어도 lazy loading은 중복 작업을 수행하지 않도록 최적화되어 있습니다. 204 | 205 | `u1.addresses`의 컬렉션에 `a1` 및 `a2` 객체들 또한 볼 수 있습니다. 206 | 207 | ```python 208 | >>> a1 209 | Address(id=4, email_address='pearl.krabs@gmail.com') 210 | >>> a2 211 | Address(id=5, email_address='pearl@aol.com') 212 | ``` 213 | 214 | `relationship` 개념에 대한 추가 소개는 이 섹션의 후반부에 더 설명드리겠습니다. 215 | 216 |
217 | 218 | ## 쿼리에서 `relationship` 사용하기 219 | 220 |
221 | 222 | 이 섹션에서는 `relationship()` 이 SQL 쿼리 구성을 자동화하는데 도움이 되는 여러 가지 방법을 소개합니다. 223 | 224 |
225 | 226 | ### `relationship()`을 사용하여 조인하기 227 | 228 | [FROM절과 JOIN명시하기] 및 [WHERE절] 섹션에서는 `Select.join()` 및 `Select.join_from()` 메서드를 사용하여 SQL JOIN을 구성하였습니다. 229 | 테이블간에 조인하는 방법을 설명하기 위해 이러한 메서드는 두 테이블을 연결하는 `ForeignKeyConstraint` 객체가 있는지 여부에 따라 ON 절을 유추하거나 특정 ON 절을 나타내는 SQL Expression 구문을 제공 할 수 있습니다. 230 | 231 | `relationship()` 객체를 사용하여 join의 ON 절을 설정할 수 있습니다. 232 | `relationship()` 에 해당하는 객체는 `Select.join()`의 **단일 인수**로 전달될 수 있으며, 233 | right join과 ON 절을 동시에 나타내는 역할을 합니다. 234 | 235 | ```python 236 | >>> print( 237 | ... select(Address.email_address). 238 | ... select_from(User). 239 | ... join(User.addresses) 240 | ... ) 241 | ``` 242 | > 위 코드는 다음 쿼리를 실행하는 것과 같습니다. 243 | ```sql 244 | SELECT address.email_address 245 | FROM user_account JOIN address ON user_account.id = address.user_id 246 | ``` 247 | 248 | 매핑된 `relationship()`있는 경우 `Select.join()` 또는 `Select.join_from()` 지정하지 않을 경우 **ON 절은 사용되지 않습니다.** 249 | 즉, `user` 및 `Address` 객체의 `relationship()` 객체가 아니라 매핑된 두 테이블 객체 간의 `ForeignKeyConstraint`로 인해 작동합니다. 250 | 251 | ```python 252 | >>> print( 253 | ... select(Address.email_address). 254 | ... join_from(User, Address) 255 | ... ) 256 | ``` 257 | > 위 코드는 다음 쿼리를 실행하는 것과 같습니다. 258 | ```sql 259 | SELECT address.email_address 260 | FROM user_account JOIN address ON user_account.id = address.user_id 261 | ``` 262 | 263 |
264 | 265 | ### 별칭(aliased)을 사용하여 조인하기 266 | 267 | `relationship()`을 사용하여 SQL JOIN을 구성하는 경우 [`PropComparator.of_type()`] 사용하여 조인 대상이 `aliased()`이 되는 사용 사례가 적합합니다. 그러나 `relationship()`를 사용하여 [`ORM Entity Aliases`]에 설명된 것과 동일한 조인을 구성합니다. 268 | 269 | ```python 270 | >>> from sqlalchemy.orm import aliased 271 | >>> address_alias_1 = aliased(Address) 272 | >>> address_alias_2 = aliased(Address) 273 | >>> print( 274 | ... select(User). 275 | ... join_from(User, address_alias_1). 276 | ... where(address_alias_1.email_address == 'patrick@aol.com'). 277 | ... join_from(User, address_alias_2). 278 | ... where(address_alias_2.email_address == 'patrick@gmail.com') 279 | ... ) 280 | ``` 281 | > 위 코드는 다음 쿼리를 실행하는 것과 같습니다. 282 | ```sql 283 | SELECT user_account.id, user_account.name, user_account.fullname 284 | FROM user_account 285 | JOIN address AS address_1 ON user_account.id = address_1.user_id 286 | JOIN address AS address_2 ON user_account.id = address_2.user_id 287 | WHERE address_1.email_address = :email_address_1 288 | AND address_2.email_address = :email_address_2 289 | ``` 290 | 291 | `relationship()`을 사용하여 `aliased()`에서 조인을 직접 사용할 수 있습니다. 292 | 293 | ```python 294 | >>> user_alias_1 = aliased(User) 295 | >>> print( 296 | ... select(user_alias_1.name). 297 | ... join(user_alias_1.addresses) 298 | ... ) 299 | ``` 300 | > 위 코드는 다음 쿼리를 실행하는 것과 같습니다. 301 | ```sql 302 | SELECT user_account_1.name 303 | FROM user_account AS user_account_1 304 | JOIN address ON user_account_1.id = address.user_id 305 | ``` 306 | 307 |
308 | 309 | ### ON 조건 확대 310 | `relation()`으로 생성된 ON 절에 조건을 추가할 수 있습니다. 이 기능은 관계된 경로에 대한 특정 조인의 범위를 신속하게 제한하는 방법뿐만 아니라 마지막 섹션에서 소개하는 로더 전략 구성과 같은 사용 사례에도 유용합니다. 311 | [`PropComparator.and_()`] 메서드는 AND를 통해 JOIN의 ON 절에 결합되는 일련의 SQL 식을 위치적으로 허용합니다. 예를 들어, 312 | `User` 및 `Address`을 활용하여 ON 기준을 특정 이메일 주소로만 제한하려는 경우 이와 같습니다. 313 | ```python 314 | >>> stmt = ( 315 | ... select(User.fullname). 316 | ... join(User.addresses.and_(Address.email_address == 'pearl.krabs@gmail.com')) 317 | ... ) 318 | 319 | >>> session.execute(stmt).all() 320 | [('Pearl Krabs',)] 321 | ``` 322 | > 위 코드는 다음 쿼리를 실행하는 것과 같습니다. 323 | ```sql 324 | SELECT user_account.fullname 325 | FROM user_account 326 | JOIN address ON user_account.id = address.user_id AND address.email_address = ? 327 | [...] ('pearl.krabs@gmail.com',) 328 | ``` 329 | 330 |
331 | 332 | ### EXISTS has() , and() 333 | [EXISTS 서브쿼리들] 섹션에서는 SQL EXISTS 키워드를 [스칼라 서브 쿼리, 상호연관 쿼리] 섹션과 함께 소개했습니다. 334 | `relationship()` 은 관계 측면에서 공통적으로 서브쿼리를 생성하는데 사용할 수 있는 일부 도움을 제공합니다. 335 | 336 |
337 | 338 | 339 | `User.addresses`와 같은 1:N (one-to-many) 관계의 경우 `PropComparator.any()`를 사용하여 `user_account`테이블과 다시 연결되는 주소 테이블에 서브쿼리를 생성할 수 있습니다. 이 메서드는 하위 쿼리와 일치하는 행을 제한하는 선택적 WHERE 기준을 허용합니다. 340 | 341 | ```python 342 | >>> stmt = ( 343 | ... select(User.fullname). 344 | ... where(User.addresses.any(Address.email_address == 'pearl.krabs@gmail.com')) 345 | ... ) 346 | 347 | >>> session.execute(stmt).all() 348 | [('Pearl Krabs',)] 349 | ``` 350 | > 위 코드는 다음 쿼리를 실행하는 것과 같습니다. 351 | ```sql 352 | SELECT user_account.fullname 353 | FROM user_account 354 | WHERE EXISTS (SELECT 1 355 | FROM address 356 | WHERE user_account.id = address.user_id AND address.email_address = ?) 357 | [...] ('pearl.krabs@gmail.com',) 358 | ``` 359 | 이와 반대로 관련된 데이터가 없는 객체를 찾는 것은 `~User.addresses.any()`을 사용하여 `User` 객체에 검색하는 방법입니다. 360 | ```python 361 | >>> stmt = ( 362 | ... select(User.fullname). 363 | ... where(~User.addresses.any()) 364 | ... ) 365 | 366 | >>> session.execute(stmt).all() 367 | [('Patrick McStar',), ('Squidward Tentacles',), ('Eugene H. Krabs',)] 368 | ``` 369 | > 위 코드는 다음 쿼리를 실행하는 것과 같습니다. 370 | ```sql 371 | SELECT user_account.fullname 372 | FROM user_account 373 | WHERE NOT (EXISTS (SELECT 1 374 | FROM address 375 | WHERE user_account.id = address.user_id)) 376 | [...] () 377 | 378 | ``` 379 | `PropComparator.has()` 메서드는 `PropComparator.any()`와 비슷한 방식으로 작동하지만, N:1 (Many-to-one) 관계에 사용됩니다. 380 | 예시로 "pearl"에 속하는 모든 `Address` 객체를 찾으려는 경우 이와 같습니다. 381 | ```python 382 | >>> stmt = ( 383 | ... select(Address.email_address). 384 | ... where(Address.user.has(User.name=="pkrabs")) 385 | ... ) 386 | 387 | >>> session.execute(stmt).all() 388 | [('pearl.krabs@gmail.com',), ('pearl@aol.com',)] 389 | ``` 390 | > 위 코드는 다음 쿼리를 실행하는 것과 같습니다. 391 | 392 | ```sql 393 | SELECT address.email_address 394 | FROM address 395 | WHERE EXISTS (SELECT 1 396 | FROM user_account 397 | WHERE user_account.id = address.user_id AND user_account.name = ?) 398 | [...] ('pkrabs',) 399 | ``` 400 | 401 |
402 | 403 | ### 관계 연산자 404 | `relationship()`와 함께 제공되는 SQL 생성 도우미에는 다음과 같은 몇 가지 종류가 있습니다. 405 | 406 | - N : 1 (Many-to-one) 비교 407 | 특정 객체 인스턴스를 N : 1 관계와 비교하여 대상 엔티티의 외부 키가 지정된 객체의 기본 키값과 일치하는 행을 선택할 수 있습니다. 408 | ```python 409 | >>> print(select(Address).where(Address.user == u1)) 410 | ``` 411 | > 위 코드는 다음 쿼리를 실행하는 것과 같습니다. 412 | ```sql 413 | SELECT address.id, address.email_address, address.user_id 414 | FROM address 415 | WHERE :param_1 = address.user_id 416 | ``` 417 | 418 | - NOT N : 1 (Many-to-one) 비교 419 | 같지 않은 연산자(!=)를 사용할 수 있습니다. 420 | ```python 421 | >>> print(select(Address).where(Address.user != u1)) 422 | ``` 423 | > 위 코드는 다음 쿼리를 실행하는 것과 같습니다. 424 | ```sql 425 | SELECT address.id, address.email_address, address.user_id 426 | FROM address 427 | WHERE address.user_id != :user_id_1 OR address.user_id IS NULL 428 | ``` 429 | 430 | - 객체가 1 : N (one-to-many) 컬렉션에 포함되어있는지 확인하는 방법입니다. 431 | ```python 432 | >>> print(select(User).where(User.addresses.contains(a1))) 433 | ``` 434 | > 위 코드는 다음 쿼리를 실행하는 것과 같습니다. 435 | ```sql 436 | SELECT user_account.id, user_account.name, user_account.fullname 437 | FROM user_account 438 | WHERE user_account.id = :param_1 439 | ``` 440 | 441 | - 객체가 1 : N 관계에서 특정 상위 항목에 있는지 확인하는 방법입니다. 442 | `with_parent()`은 주어진 상위 항목이 참조하는 행을 반환하는 비교를 생성합니다. 이는 == 연산자를 사용하는 것과 동일합니다. 443 | ```python 444 | >>> from sqlalchemy.orm import with_parent 445 | >>> print(select(Address).where(with_parent(u1, User.addresses))) 446 | ``` 447 | > 위 코드는 다음 쿼리를 실행하는 것과 같습니다. 448 | ```sql 449 | SELECT address.id, address.email_address, address.user_id 450 | FROM address 451 | WHERE :param_1 = address.user_id 452 | ``` 453 | 454 |
455 | 456 | ## Loading relationshiop의 종류 457 | 458 |
459 | 460 | [`관계 로드`](#관계-로드) 섹션에서는 매핑된 객체 인스턴스로 작업할 때 `relationship()`을 사용하여 매핑된 특성에 엑세스하면 이 컬렉션에 있어야 하는 객체를 로드하며, 컬렉션이 채워지지 않은 경우 `lazy load`가 발생한다는 개념을 도입했습니다. 461 | 462 | Lazy loading 방식은 가장 유명한 ORM 패턴 중 하나이며, 가장 논란이 많은 ORM 패턴이기도 합니다. 463 | 메모리에 있는 수십개의 ORM 객체가 각각 소수의 언로드 속성을 참조하는 경우, 객체의 일상적인 조작은 누적이 될 수 있는 많은 문제([`N+1 Problem`])를 암묵적으로 방출될 수 있습니다. 이러한 암시적 쿼리는 더 이상 사용할 수 없는 데이터베이스 변환을 시도할 때 또는 비동기화 같은 대체 동시성 패턴을 사용할 때 실제로 전혀 작동하지 않을 수 있습니다. 464 | 465 | > [`N + 1 Problem`]이란? 466 | 쿼리 1번으로 N건의 데이터를 가져왔는데 원하는 데이터를 얻기 위해 이 N건의 데이터를 데이터 수 만큼 반복해서 2차적으로 쿼리를 수행하는 문제입니다. 467 | 468 | lazy loading 방식은 사용 중인 동시성 접근법과 호환되고 다른 방법으로 문제를 일으키지 않을 때 매우 인기있고 유용한 패턴입니다. 이러한 이유로 SQLAlchemy의 ORM은 이러한 로드 동작을 제허하고 최적화할 수 있는 기능에 중점을 둡니다. 469 | 470 | 무엇보다 ORM의 lazy loading 방식을 효과적으로 사용하는 첫 번째 단계는 **Application을 테스트하고 SQL을 확인하는 것입니다.** 471 | `Session`에서 분리된 객체에 대해 로드가 부적절하게 발생하는 경우, **[`Loading relationship의 종류`](#loading-relationshiop의-종류)** 사용을 검토해야 합니다. 472 | 473 | `Select.options()` 메서드를 사용하여 SELECT 문과 연결할 수 있는 객체로 표시됩니다. 474 | ```python 475 | for user_obj in session.execute( 476 | select(User).options(selectinload(User.addresses)) 477 | ).scalars(): 478 | user_obj.addresses # access addresses collection already loaded 479 | ``` 480 | `relationship.lazy`를 사용하여 `relationship()`의 기본값으로 구성할 수도 있습니다. 481 | ```sql 482 | from sqlalchemy.orm import relationship 483 | class User(Base): 484 | __tablename__ = 'user_account' 485 | 486 | addresses = relationship("Address", back_populates="user", lazy="selectin") 487 | ``` 488 | 489 | 가장 많이 사용되는 loading 방식 몇 가지를 소개합니다. 490 | 491 | > 참고 492 | **관계 로딩 기법의 2가지 기법** 493 | [`Configuring Loader Strategies at Mapping Time`] - `relationship()` 구성에 대한 세부정보 494 | [`Relationship Loading with Loader Options`] - 로더에 대한 세부정보 495 | 496 |
497 | 498 | ### Select IN loading 방식 499 | 최신 SQLAlchemy에서 가장 유용한 로딩방식 옵션은 `selectinload()`입니다. 이 옵션은 관련 컬렉션을 참조하는 객체 집합의 문제인 가장 일반적인 형태의 "N + 1 Problem"문제를 해결합니다. 500 | 대부분의 경우 JOIN 또는 하위 쿼리를 도입하지 않고 관련 테이블에 대해서만 내보낼 수 있는 SELET 양식을 사용하여 이 작업을 수행합니다. 또한 컬렉션이 로드되지 않은 상위 객체에 대한 쿼리만 수행합니다. 501 | 아래 예시는 `User` 객체와 관련된 `Address` 객체를 `selectinload()`하여 보여줍니다. 502 | `Session.execute()` 호출하는 동안 데이터베이스에서는 두 개의 SELECT 문이 생성되고 두 번째는 관련 `Address` 객체를 가져오는 것입니다. 503 | ```sql 504 | >>> from sqlalchemy.orm import selectinload 505 | >>> stmt = ( 506 | ... select(User).options(selectinload(User.addresses)).order_by(User.id) 507 | ... ) 508 | >>> for row in session.execute(stmt): 509 | ... print(f"{row.User.name} ({', '.join(a.email_address for a in row.User.addresses)})") 510 | spongebob (spongebob@sqlalchemy.org) 511 | sandy (sandy@sqlalchemy.org, sandy@squirrelpower.org) 512 | patrick () 513 | squidward () 514 | ehkrabs () 515 | pkrabs (pearl.krabs@gmail.com, pearl@aol.com) 516 | ``` 517 | > 위 코드는 다음 쿼리를 실행하는 것과 같습니다. 518 | ```sql 519 | SELECT user_account.id, user_account.name, user_account.fullname 520 | FROM user_account ORDER BY user_account.id 521 | [...] () 522 | SELECT address.user_id AS address_user_id, address.id AS address_id, 523 | address.email_address AS address_email_address 524 | FROM address 525 | WHERE address.user_id IN (?, ?, ?, ?, ?, ?) 526 | [...] (1, 2, 3, 4, 5, 6) 527 | ``` 528 | 529 |
530 | 531 | ### Joined Loading 방식 532 | `Joined Loading`은 SQLAlchemy에서 가장 오래됬으며, 이 방식은 eager loading의 일종으로 `joined eager loading`이라고도 합니다. N : 1 관계의 객체를 로드하는 데 가장 적합하며, 533 | `relationship()`에 명시된 테이블을 SELECT JOIN하여 모든 테이블의 데이터들을 한꺼번에 가져오는 방식으로 `Address` 객체에 연결된 사용자가 있는 다음과 같은 경우에 OUTER JOIN이 아닌 INNER JOIN을 사용할 수 있습니다. 534 | ```python 535 | >>> from sqlalchemy.orm import joinedload 536 | >>> stmt = ( 537 | ... select(Address).options(joinedload(Address.user, innerjoin=True)).order_by(Address.id) 538 | ... ) 539 | >>> for row in session.execute(stmt): 540 | ... print(f"{row.Address.email_address} {row.Address.user.name}") 541 | 542 | spongebob@sqlalchemy.org spongebob 543 | sandy@sqlalchemy.org sandy 544 | sandy@squirrelpower.org sandy 545 | pearl.krabs@gmail.com pkrabs 546 | pearl@aol.com pkrabs 547 | ``` 548 | > 위 코드는 다음 쿼리를 실행하는 것과 같습니다. 549 | ```sql 550 | SELECT address.id, address.email_address, address.user_id, user_account_1.id AS id_1, 551 | user_account_1.name, user_account_1.fullname 552 | FROM address 553 | JOIN user_account AS user_account_1 ON user_account_1.id = address.user_id 554 | ORDER BY address.id 555 | [...] () 556 | ``` 557 | 558 | `joinedload()`는 1 : N 관계를 의미하는 컬렉션에도 사용되지만 중접 컬렉션 및 더 큰 컬렉션이므로 `selectinload()` 처럼 사례별로 평가해야 하는 것과 같은 다른 옵션과 비교 합니다. 559 | 560 | 561 | SELECT 쿼리문의 WHERE 및 ORDER BY 기준은 **joinload()에 의해 렌더링된 테이블을 대상으로 하지 않는다는** 점에 유의하는 것이 중요합니다. 위 SQL 쿼리에서 직접 주소를 지정할 수 없는 *익명 별칭**이 `user_account`테이블에 적용된 것을 볼 수 있습니다. 이 개념은 [`Zen of joined Eager Loading`] 섹션에서 더 자세히 설명합니다. 562 | 563 | `joinedload()`에 의해 ON 절은 이전 [`ON 조건 확대`](#on-조건-확대)에서 설명한 방법 `joinedload()`을 사용하여 직접 영향을 받을 수 있습니다. 564 | 565 | > 참고 566 | 일반적인 경우에는 "N + 1 problem"가 훨씬 덜 만연하기 때문에 다대일 열망 로드가 종종 필요하지 않다는 점에 유의하는 것이 중요합니다. 많은 객체가 모두 동일한 관련 객체를 참조하는 경우(예: `Address` 각각 동일한 참조하는 많은 객체) 일반 지연 로드를 사용하여 `User`객체에 대해 SQL이 한 번만 내보내 집니다. 지연 로드 루틴은 `Session`가능한 경우 SQL을 내보내지 않고 현재 기본 키로 관련 객체를 조회 합니다. 567 | 568 |
569 | 570 | ### Explicit Join + Eager load 방식 571 | 572 | 일반적인 사용 사례는 `contains_eager()`옵션을 사용하며, 이 옵션은 JOIN을 직접 설정했다고 가정하고 대신 COLUMNS 절의 추가 열이 반환된 각 객체의 관련 속성에 로드해야 한다는 점을 제외하고는 `joinedload()` 와 매우 유사합니다. 573 | 574 | ```python 575 | >>> from sqlalchemy.orm import contains_eager 576 | 577 | >>> stmt = ( 578 | ... select(Address). 579 | ... join(Address.user). 580 | ... where(User.name == 'pkrabs'). 581 | ... options(contains_eager(Address.user)).order_by(Address.id) 582 | ... ) 583 | 584 | >>> for row in session.execute(stmt): 585 | ... print(f"{row.Address.email_address} {row.Address.user.name}") 586 | 587 | pearl.krabs@gmail.com pkrabs 588 | pearl@aol.com pkrabs 589 | ``` 590 | > 위 코드는 다음 쿼리를 실행하는 것과 같습니다. 591 | ```sql 592 | SELECT user_account.id, user_account.name, user_account.fullname, 593 | address.id AS id_1, address.email_address, address.user_id 594 | FROM address JOIN user_account ON user_account.id = address.user_id 595 | WHERE user_account.name = ? ORDER BY address.id 596 | [...] ('pkrabs',) 597 | ``` 598 | 위에서 `user_account.name`을 필터링하고 `user_account`의 반환된 `Address.user`속성으로 로드했습니다. 599 | `joinedload()`를 별도로 적용했다면 불필요하게 두 번 조인된 SQL 쿼리가 생성되었을 것입니다. 600 | 601 | ```python 602 | >>> stmt = ( 603 | ... select(Address). 604 | ... join(Address.user). 605 | ... where(User.name == 'pkrabs'). 606 | ... options(joinedload(Address.user)).order_by(Address.id) 607 | ... ) 608 | >>> print(stmt) # SELECT has a JOIN and LEFT OUTER JOIN unnecessarily 609 | ``` 610 | > 위 코드는 다음 쿼리를 실행하는 것과 같습니다. 611 | ```sql 612 | SELECT address.id, address.email_address, address.user_id, 613 | user_account_1.id AS id_1, user_account_1.name, user_account_1.fullname 614 | FROM address JOIN user_account ON user_account.id = address.user_id 615 | LEFT OUTER JOIN user_account AS user_account_1 ON user_account_1.id = address.user_id 616 | WHERE user_account.name = :name_1 ORDER BY address.id 617 | ``` 618 | 619 |
620 | 621 | > 참고 622 | **관계 로딩 기법의 2가지 기법** 623 | [`Zen of joined Eager Loading`] - 해당 로딩 방식에 대한 세부정보 624 | [`Routing Explicit Joins/Statements into Eagerly Loaded Collections`] - using `contains_eager()` 625 | 626 |
627 | 628 | ### 로더 경로 설정 629 | `PropComparator.and_()` 방법은 실제로 대부분의 로더 옵션에서 일반적으로 사용할 수 있습니다. 630 | 예를 들어 `sqlalchemy.org`도메인에서 사용자 이름과 이메일 주소를 다시 로드하려는 경우 `selectinload()` 전달된 인수에 `PropComparator.and_()`를 적용하여 다음 조건을 제한할 수 있습니다. 631 | ```python 632 | >>> from sqlalchemy.orm import selectinload 633 | >>> stmt = ( 634 | ... select(User). 635 | ... options( 636 | ... selectinload( 637 | ... User.addresses.and_( 638 | ... ~Address.email_address.endswith("sqlalchemy.org") 639 | ... ) 640 | ... ) 641 | ... ). 642 | ... order_by(User.id). 643 | ... execution_options(populate_existing=True) 644 | ... ) 645 | 646 | >>> for row in session.execute(stmt): 647 | ... print(f"{row.User.name} ({', '.join(a.email_address for a in row.User.addresses)})") 648 | 649 | spongebob () 650 | sandy (sandy@squirrelpower.org) 651 | patrick () 652 | squidward () 653 | ehkrabs () 654 | pkrabs (pearl.krabs@gmail.com, pearl@aol.com) 655 | ``` 656 | > 위 코드는 다음 쿼리를 실행하는 것과 같습니다. 657 | ```sql 658 | SELECT user_account.id, user_account.name, user_account.fullname 659 | FROM user_account ORDER BY user_account.id 660 | [...] () 661 | SELECT address.user_id AS address_user_id, address.id AS address_id, 662 | address.email_address AS address_email_address 663 | FROM address 664 | WHERE address.user_id IN (?, ?, ?, ?, ?, ?) 665 | AND (address.email_address NOT LIKE '%' || ?) 666 | [...] (1, 2, 3, 4, 5, 6, 'sqlalchemy.org') 667 | ``` 668 | 위에서 매우 중요한 점은 `.execution_options(populate_existing=True)` 옵션이 추가되었다는 점 입니다. 669 | 행을 가져올 때 적용되는 이 옵션은 로더 옵션이 이미 로드된 객체의 기존 컬렉션 내용을 대체해야 함을 나타냅니다. 670 | `Session`객체로 반복 작업하므로 위에서 로드되는 객체는 본 튜토리얼의 ORM 섹션 시작 시 처음 유지되었던 것과 동일한 Python 인스턴스입니다. 671 | 672 |
673 | 674 | ### raise loading 방식 675 | `raiseload()`옵션은 일반적으로 느린 대신 오류를 발생시켜 N + 1 문제가 발생하는 것을 완전히 차단하는데 사용됩니다. 676 | 예로 두 가지 변형 모델이 있습니다. SQL이 필요한 `lazy load` 와 현재 `Session`만 참조하면 되는 작업을 포함한 모든 "load" 작업을 차단하는 `raiseload.sql_only` 옵션입니다. 677 | 678 | ```python 679 | class User(Base): 680 | __tablename__ = 'user_account' 681 | 682 | # ... Column mappings 683 | 684 | addresses = relationship("Address", back_populates="user", lazy="raise_on_sql") 685 | 686 | 687 | class Address(Base): 688 | __tablename__ = 'address' 689 | 690 | # ... Column mappings 691 | 692 | user = relationship("User", back_populates="addresses", lazy="raise_on_sql") 693 | ``` 694 | 695 | 이러한 매핑을 사용하면 응용 프로그램이 'lazy loading'에 차단되어 특정 쿼리에 로더 전략을 지정해야 합니다. 696 | 697 | ```python 698 | u1 = s.execute(select(User)).scalars().first() 699 | u1.addresses 700 | sqlalchemy.exc.InvalidRequestError: 'User.addresses' is not available due to lazy='raise_on_sql' 701 | ``` 702 | 703 | 예외는 이 컬렉션을 대신 먼저 로드해야 함을 나타냅니다. 704 | ```python 705 | u1 = s.execute(select(User).options(selectinload(User.addresses))).scalars().first() 706 | ``` 707 | 708 | `lazy="raise_on_sql"` 옵션은 N : 1 관계에도 현명하게 시도합니다. 709 | 위에서 `Address.user`속성이 `Address`에 로드되지 않았지만 해당 `User` 객체가 동일한 `Session`에 있는 경우 "raiseload"은 오류를 발생시키지 않습니다. 710 | 711 | > 참고 712 | [`raiseload`를 사용하여 원치 않는 lazy loading 방지] 713 | [`relatonship`에서 lazy loading 방지] 714 | 715 | 716 | 717 | 718 | [테이블에 매핑할 클래스 선언]: (https://soogoonsoogoonpythonists.github.io/sqlalchemy-for-pythonist/tutorial/4.%20%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4%20%EB%A9%94%ED%83%80%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%A1%9C%20%EC%9E%91%EC%97%85%ED%95%98%EA%B8%B0.html#%E1%84%90%E1%85%A6%E1%84%8B%E1%85%B5%E1%84%87%E1%85%B3%E1%86%AF%E1%84%8B%E1%85%A6-%E1%84%86%E1%85%A2%E1%84%91%E1%85%B5%E1%86%BC%E1%84%92%E1%85%A1%E1%86%AF-%E1%84%8F%E1%85%B3%E1%86%AF%E1%84%85%E1%85%A2%E1%84%89%E1%85%B3-%E1%84%89%E1%85%A5%E1%86%AB%E1%84%8B%E1%85%A5%E1%86%AB) 719 | 720 | [ORM으로 행 삽입하기]: (https://soogoonsoogoonpythonists.github.io/sqlalchemy-for-pythonist/tutorial/6.%20ORM%EC%9C%BC%EB%A1%9C%20%EB%8D%B0%EC%9D%B4%ED%84%B0%20%EC%A1%B0%EC%9E%91%ED%95%98%EA%B8%B0.html#orm%E1%84%8B%E1%85%B3%E1%84%85%E1%85%A9-%E1%84%92%E1%85%A2%E1%86%BC-%E1%84%89%E1%85%A1%E1%86%B8%E1%84%8B%E1%85%B5%E1%86%B8%E1%84%92%E1%85%A1%E1%84%80%E1%85%B5) 721 | [일시적인]: (https://docs.sqlalchemy.org/en/14/glossary.html#term-transient) 722 | [보류]: (https://docs.sqlalchemy.org/en/14/glossary.html#term-pending) 723 | 724 | [FROM절과 JOIN명시하기]: (https://soogoonsoogoonpythonists.github.io/sqlalchemy-for-pythonist/tutorial/5.%20%EB%8D%B0%EC%9D%B4%ED%84%B0%20%ED%95%B8%EB%93%A4%EB%A7%81%20-%20Core,%20ORM%EC%9C%BC%EB%A1%9C%20%ED%96%89%20%EC%A1%B0%ED%9A%8C%ED%95%98%EA%B8%B0.html#from%E1%84%8C%E1%85%A5%E1%86%AF%E1%84%80%E1%85%AA-join-%E1%84%86%E1%85%A7%E1%86%BC%E1%84%89%E1%85%B5%E1%84%92%E1%85%A1%E1%84%80%E1%85%B5) 725 | 726 | [WHERE절]: (https://soogoonsoogoonpythonists.github.io/sqlalchemy-for-pythonist/tutorial/5.%20%EB%8D%B0%EC%9D%B4%ED%84%B0%20%ED%95%B8%EB%93%A4%EB%A7%81%20-%20Core,%20ORM%EC%9C%BC%EB%A1%9C%20%ED%96%89%20%EC%A1%B0%ED%9A%8C%ED%95%98%EA%B8%B0.html#where%E1%84%8C%E1%85%A5%E1%86%AF) 727 | 728 | 729 | [`PropComparator.and_()`]: (https://docs.sqlalchemy.org/en/14/orm/internals.html#sqlalchemy.orm.PropComparator.and_) 730 | 731 | [EXISTS 서브쿼리들]: (https://soogoonsoogoonpythonists.github.io/sqlalchemy-for-pythonist/tutorial/5.%20%EB%8D%B0%EC%9D%B4%ED%84%B0%20%ED%95%B8%EB%93%A4%EB%A7%81%20-%20Core,%20ORM%EC%9C%BC%EB%A1%9C%20%ED%96%89%20%EC%A1%B0%ED%9A%8C%ED%95%98%EA%B8%B0.html#exists-%E1%84%89%E1%85%A5%E1%84%87%E1%85%B3%E1%84%8F%E1%85%AF%E1%84%85%E1%85%B5%E1%84%83%E1%85%B3%E1%86%AF) 732 | 733 | [스칼라 서브 쿼리, 상호연관 쿼리]: (https://soogoonsoogoonpythonists.github.io/sqlalchemy-for-pythonist/tutorial/5.%20%EB%8D%B0%EC%9D%B4%ED%84%B0%20%ED%95%B8%EB%93%A4%EB%A7%81%20-%20Core,%20ORM%EC%9C%BC%EB%A1%9C%20%ED%96%89%20%EC%A1%B0%ED%9A%8C%ED%95%98%EA%B8%B0.html#%E1%84%89%E1%85%B3%E1%84%8F%E1%85%A1%E1%86%AF%E1%84%85%E1%85%A1-%E1%84%89%E1%85%A5%E1%84%87%E1%85%B3-%E1%84%8F%E1%85%AF%E1%84%85%E1%85%B5-%E1%84%89%E1%85%A1%E1%86%BC%E1%84%92%E1%85%A9%E1%84%8B%E1%85%A7%E1%86%AB%E1%84%80%E1%85%AA%E1%86%AB-%E1%84%8F%E1%85%AF%E1%84%85%E1%85%B5) 734 | 735 | [`N + 1 Problem`]: (https://blog.naver.com/yysdntjq/222405755893) 736 | 737 | [`N+1 Problem`]: (https://docs.sqlalchemy.org/en/14/glossary.html#term-N-plus-one-problem) 738 | 739 | [`Zen of joined Eager Loading`]: (https://docs.sqlalchemy.org/en/14/orm/loading_relationships.html#zen-of-eager-loading) 740 | 741 | [`Zen of joined Eager Loading`]: (https://docs.sqlalchemy.org/en/14/orm/loading_relationships.html#zen-of-eager-loading) 742 | 743 | [`Routing Explicit Joins/Statements into Eagerly Loaded Collections`]: (https://docs.sqlalchemy.org/en/14/orm/loading_relationships.html#contains-eager) 744 | 745 | [`Configuring Loader Strategies at Mapping Time`]: (https://docs.sqlalchemy.org/en/14/orm/loading_relationships.html#relationship-lazy-option) 746 | 747 | 748 | [`Relationship Loading with Loader Options`]: (https://docs.sqlalchemy.org/en/14/orm/loading_relationships.html#relationship-loader-options) 749 | 750 | [`raiseload`를 사용하여 원치 않는 lazy loading 방지]: (https://docs.sqlalchemy.org/en/14/orm/loading_relationships.html#prevent-lazy-with-raiseload) 751 | 752 | [`relatonship`에서 lazy loading 방지]: (https://docs.sqlalchemy.org/en/14/orm/loading_relationships.html) 753 | -------------------------------------------------------------------------------- /src/tutorial/README.md: -------------------------------------------------------------------------------- 1 | # Tutorial 2 | 3 | 이 문서는 [SQLAlchemy 1.4/2.0 Tutorial](https://docs.sqlalchemy.org/en/14/tutorial/)를 번역 및 정리한 글입니다. 4 | 5 | 기존 공식 문서는 보기 어렵고, 너무 많은 내용이 담겨있습니다. 또한 초보자가 보기에 너무 어렵다는 생각이 들었습니다. 6 | 이에 SQLAlchemy Tutorial 문서를 같이 공부할 겸, 누구나 쉽게 볼 수 있고 정리하면 어떨까라는 생각으로 [이 글](https://github.com/SoogoonSoogoonPythonists/sqlalchemy-for-pythonist/issues/2)에서 같이 작업할 사람들이 모이게 되었습니다. 7 | 8 | 한 달이 조금 넘는 기간동안, 매주 돌아가며 공식 Tutorial 문서를 한 챕터씩 맡아서 작업했습니다. 9 | 서로 리뷰해주며 글을 다듬고, 이해가 안가는 부분을 서로 물어보기도 했습니다. 10 | 이 문서 모음들은 이러한 과정 끝에 나온 결과물입니다. 저희가 스터디한 흔적이기도 합니다. 11 | 12 | 여전히 부족한 부분이 많고, 틀린 부분이 있을 수 있습니다. 13 | 이런 부분 발견하시면 언제든 기여해주세요. 늘 환영하고 있습니다. 14 | 15 | 아래와 같은 분들이 참여해주셨습니다. 16 | 17 | 18 | 19 | 20 | --------------------------------------------------------------------------------