Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

PlaySQLAlchemyORM2017.key

833 views

Published on

神戸Pythonの会 #14 の講演資料です。

Published in: Engineering
  • Be the first to comment

PlaySQLAlchemyORM2017.key

  1. 1. SQLAlchemy 入門 ORM編 for Kobe Python Meetup #14 2017/10/20 Kobe Japan
  2. 2. Yasushi Masuda PhD ( @whosaysni ) Tech team, Core IT grp. IT Dept. MonotaRO Co., LTD. Pythonista since 2001 (2.0~) • elaphe (barcode library) • oikami.py (老神.py) • PyCon JP founder 翻訳もろもろ
  3. 3. 参考文献 オンラインドキュメント:
 http://docs.sqlalchemy.org/en/rel_1_1/
 
 (古いけど)和訳:
 http://omake.accense.com/static/doc-ja/ sqlalchemy/
  4. 4. 準備 sakila DB SQLite版 https://github.com/jOOQ/jOOQ/jOOQ-examples/Sakila/sqlite-sakila-db/ Sakila • MySQL エンジニア 作のサンプルDB • レンタルビデオ屋
 (...通じます?) のモ デル • BSD ライセンス スキーマのドキュメントは下記 https://dev.mysql.com/doc/sakila/en/sakila-structure-tables.html (上記のサイトのSQLiteデータベースバイナリは壊れているので、以下から取得してください) https://github.com/wallymathieu/sakila-sample-database-ports/blob/master/sqlite-sakila-db/sqlite-sakila.sq
  5. 5. アジェンダ 前回のあらすじ automapでORMを体験しよう
 
 おまけ:そのほかのマッピング方法
  6. 6. 前回のあらすじ SQLAlchemyを3行で エンジンを使ってみましょう
 
 SQL式を使ってみましょう ORMを使ってみましょう
  7. 7. # クエリはエンジンで実行する engine = create_engine(engine_url) engine.execute('select ... from ...')
 # SQLを表すクエリオブジェクトを構築できる film = table('film', column('film_id'), ...) q = select([film]).where(film.c.film_id==1) engine.execute(q) # スキーマ定義を使うとDDLも生成・実行できる film = Table('Film', MetaData(), Column('film_id', INTEGER), ...) f.create(bind=engine) engine.execute(select([film.c.film_id, ...])).fetchone() # 宣言的ORMはクラスでテーブル構造を表現する Base = declarative_base() class Film(Base): film_id = Column(INTEGER, primary_key=True) ... # ORMでのレコードの操作にはセッションを使う Session = sessionmaker(bind=engine) session = Session() session.query(Film).get(1) f = session.query(Film).filter(Film.film_id>3).first() f.rating = 'PG-18' # カラム値の変更 session.flush() # 保存
  8. 8. ORM
  9. 9. ORMでできたら嬉しいこと再確認 • DBレコードをオブジェクトのように扱いたい • 1レコード1オブジェクト • 別テーブルのレコード参照→別クラスのオブジェクトの参照 • オブジェクトのデータをDBにシリアライズしたい • インスタンスを保存・あとで取り出したい • DBの機能を使ってオブジェクトを操作したい • オブジェクトの検索・フィルタリング
  10. 10. SQLAlchemyのORM • 1つのテーブルを1つのクラスにマッピングする • テーブルのレコードがクラスのインスタンス • 別テーブルのレコード(のオブジェクト)は、マッピングクラスにリレー ション (relation) を定義して参照できる • セッションを使ってオブジェクトデータをDBから読み出し・保存できる • セッションのフラッシュ(精算)で、オブジェクトとDBの状態とが一致する • セッションのコミット(承認)で、変更内容がDBに永続化される • オブジェクトを取り出すときに SQL式 で検索条件を設定できる
  11. 11. ORM操作の流れ マッピングを設定する セッションクラスを設定する セッションインスタンスを作る オブジェクトを
 DBから取り出す オブジェクトを クラスから生成する セッションに
 追加する オブジェクトを 更新する セッションをflush/rollback マッピング 関連の操作 セッションの
 操作 セッションを破棄 毎回セッションを 作る時のサイクル セッションを 使い続けるときの サイクル
  12. 12. classic(古典的) mapping
 古いやつ昔からあるやつ
 新しいやつを支えている
 古の魔法 = よくわからんから避けられる
 declarative(宣言的) mapping
 新しいやつ 書きやすい 流行の宣言的API
 古の魔法x現代の魔法=よくわからないけど便利
 SAでORMといえばだいたいこっちの話
 automapping
 宣言的マッピングをある程度自動化してくれる
 ある意味最強の呪文 SQLAlchemyのマッピング
  13. 13. 古典的マッピング Tableオブジェクト Column foo Column bar Column baz ... エンティティクラス マッピングクラス 仕掛けの付いた foo 仕掛けの付いた bar 仕掛けの付いた baz ... method qux method quux method qux method quux ... その他有象無象 ... その他有象無象 mapperの仕掛けた
 内部オブジェクト 
 mapper
  14. 14. 宣言ベースクラス マッピングクラスの 定義 宣言的マッピング Column foo Column bar Column baz ... マッピングクラス 仕掛けの付いた foo 仕掛けの付いた bar 仕掛けの付いた baz ... method qux method quux method qux method quux ... その他有象無象 ... その他有象無象 内部オブジェクト Tableオブジェクト 宣言ベースクラスの機能 Column foo Column bar Column baz mapping
  15. 15. 自動マッピング
 ベースクラス マッピングクラスの 定義 自動マッピング マッピングクラス 仕掛けの付いた foo 仕掛けの付いた bar 仕掛けの付いた baz ... method qux method quux method qux method quux ... その他有象無象 ... その他有象無象 得体の知れない
 内部オブジェクト Tableオブジェクト ベースクラス機能 Column foo Column bar Column baz (スキーマ定義) DB上のスキーマ構造 mapping
  16. 16. セッション • セッション:トランザクションのようなもの
 エンジンのDB接続一つが対応している
 通常、セッションの持続中は、他のプログラムは
 セッションが捕まえている接続にアクセスできない • オブジェクトの読み出し:SELECT
 新たなオブジェクトをセッションに追加:INSERT
 オブジェクトのアトリビュートを更新:UPDATE
 セッションにオブジェクトをdeleteさせる:DELETE • 必要に応じてクエリを実行し、DB(のトランザクション)と
 状態を同期する • 通常は、セッションをcommit()するとDBを更新する
 (トランザクションをCOMMITする)
  17. 17. セッション Program query A SELECT A record Aobject A start
 tracking A ... (updates) flag A as "dirty" reflect new/dirty changes flush() start
 tracking B ... add(b) UPDATE A INSERT B COMMIT BEGIN (create) Database (new object) commit()
  18. 18. 使ってみましょう • automapを使って楽してマッピングを設定しましょう
 (declarativeやclassic mappingは後で)
 • セッションを作成して、マッピングを使ってみましょう
  19. 19. ORM操作の流れ マッピングを設定する セッションクラスを設定する セッションインスタンスを作る オブジェクトを
 DBから取り出す オブジェクトを クラスから生成する セッションに
 追加する オブジェクトを 更新する セッションをflush/commit/rollback マッピング 関連の操作 セッションの
 操作 セッションを破棄 毎回セッションを 作る時のサイクル セッションを 使い続けるときの サイクル
  20. 20. マッピングベースクラスを作る ↓ エンジンを指定してマッピングを準備する (DBからスキーマを呼び出してマッピングする) automappingの流れ
  21. 21. # SQLエコーバックモードを有効にして接続する >>> from sqlalchemy import create_engine >>> e = create_engine('sqlite:///sqlite-sakila.sq', echo=True) # filmテーブルがあるか確かめる >>> e.execute('select count(*) from film').scalar() 2017-10-01 07:27:45,014 INFO sqlalchemy.engine.base.Engine select count(*) from film 2017-10-01 07:27:45,014 INFO sqlalchemy.engine.base.Engine () 1002 >>> automappingを使ってみましょう
 (エンジンの生成) ↑SQLAlchemyのエコーバック
  22. 22. # 自動マッピングベースクラスを生成 >>> from sqlalchemy.ext.automap import automap_base >>> Base = automap_base() # エンジンを指定してテーブル情報を反映させる >>> Base.prepare(engine=e, reflect=True) 2017-10-01 07:37:18,014 INFO sqlalchemy.engine.base.Engine SELECT CAST .... ... 2017-10-01 07:37:18,019 INFO sqlalchemy.engine.base.Engine SELECT name FROM sqlite_master WHERE type='table' ORDER BY name 2017-10-01 07:37:18,019 INFO sqlalchemy.engine.base.Engine () 2017-10-01 07:37:18,021 INFO sqlalchemy.engine.base.Engine PRAGMA table_info("actor") 2017-10-01 07:37:18,022 INFO sqlalchemy.engine.base.Engine () 2017-10-01 07:37:18,023 INFO sqlalchemy.engine.base.Engine SELECT sql FROM (SELECT * FROM sqlite_master UNION ALL SELECT * FROM sqlite_temp_master) WHERE name = 'actor' AND type = 'table' 2017-10-01 07:37:18,023 INFO sqlalchemy.engine.base.Engine () 2017-10-01 07:37:18,024 INFO sqlalchemy.engine.base.Engine PRAGMA foreign_key_list("actor") 2017-10-01 07:37:18,024 INFO sqlalchemy.engine.base.Engine () 2017-10-01 07:37:18,024 INFO sqlalchemy.engine.base.Engine SELECT sql FROM (SELECT * FROM sqlite_master UNION ALL SELECT * FROM sqlite_temp_master) WHERE name = 'actor' AND type = 'table' 2017-10-01 07:37:18,024 INFO sqlalchemy.engine.base.Engine () 2017-10-01 07:37:18,026 INFO sqlalchemy.engine.base.Engine PRAGMA index_list("actor") 2017-10-01 07:37:18,026 INFO sqlalchemy.engine.base.Engine () ... >>> list(Base.classes) # テーブルが読み込めている [<class 'sqlalchemy.ext.automap.category'>, <class 'sqlalchemy.ext.automap.city'>, <class 'sqlalchemy.ext.automap.store'>, <class 'sqlalchemy.ext.automap.film_text'>, <class 'sqlalchemy.ext.automap.language'>, <class 'sqlalchemy.ext.automap.country'>, <class 'sqlalchemy.ext.automap.actor'>, <class 'sqlalchemy.ext.automap.film_category'>, <class 'sqlalchemy.ext.automap.customer'>, <class 'sqlalchemy.ext.automap.film_actor'>, <class 'sqlalchemy.ext.automap.inventory'>, <class 'sqlalchemy.ext.automap.address'>, <class 'sqlalchemy.ext.automap.staff'>, <class 'sqlalchemy.ext.automap.rental'>, <class 'sqlalchemy.ext.automap.payment'>, <class 'sqlalchemy.ext.automap.film'>, <class 'sqlalchemy.ext.automap.location'>] >>> automappingを使ってみましょう
 (ベースクラスの準備) ↓テーブル一覧を取得 テーブルの情報を取得
  23. 23. # Base.classes は(自動的に作られた)テーブル名でアクセスできる >>> Film = Base.classes.film >>> Film <class 'sqlalchemy.ext.automap.film'> # Filmクラスはマッピング済み >>> dir(Film) ['__abstract__', '__class__', ... , 'description', 'film_id', 'language', 'language_id', 'last_update', 'length', 'metadata', 'original_language_id', 'prepare', 'rating', 'release_year', 'rental_duration', 'rental_rate', 'replacement_cost', 'special_features', 'title'] # スキーマ定義もできている >>> Film.__table__ Table('film', MetaData(bind=None), Column('film_id', INTEGER(), table=<film>, primary_key=True, ...), Column('title', VARCHAR(length=255), table=<film>, nullable=False), ...) >>> automappingを使ってみましょう
 (マッピングクラスを参照する)
  24. 24. ORM操作の流れ マッピングを設定する セッションクラスを設定する セッションインスタンスを作る オブジェクトを
 DBから取り出す オブジェクトを クラスから生成する セッションに
 追加する オブジェクトを 更新する セッションをflush/commit/rollback マッピング 関連の操作 セッションの
 操作 セッションを破棄 毎回セッションを 作る時のサイクル セッションを 使い続けるときの サイクル
  25. 25. セッションを作ってみましょう
 (セッションの生成) # セッションを作るクラスを sessionmaker で作る # 使うエンジンが一定ならこのとき指定する >>> from sqlalchemy.orm import sessionmaker >>> Session = sessionmaker(bind=e) >>> session = Session() >>> session <sqlalchemy.orm.session.Session object at ...> # dir(session) してみましょう
  26. 26. ORM操作の流れ マッピングを設定する セッションクラスを設定する セッションインスタンスを作る オブジェクトを
 DBから取り出す オブジェクトを クラスから生成する セッションに
 追加する オブジェクトを 更新する セッションをflush/commit/rollback マッピング 関連の操作 セッションの
 操作 セッションを破棄 毎回セッションを 作る時のサイクル セッションを 使い続けるときの サイクル
  27. 27. DBからオブジェクトを取り出しましょう
 (クエリを生成する) # session.query() は Model クラスを問い合わせるための # クエリオブジェクトを返す >>> Film = Base.classes.film >>> q = session.query(Film) >>> q <sqlalchemy.orm.query.Query object at ...> # str(q) するとクエリを返す(実行はされない) >>> str(q) 'SELECT film.film_id AS film_film_id, ... FROM film'
  28. 28. DBからオブジェクトを取り出しましょう
 (クエリを実行する) # 実際にオブジェクトを取り出す操作を実行したときに
 # クエリが実行される >>> q.get(1) # プライマリキーが 1 2017-10-01 23:29:52,678 INFO sqlalchemy.engine.base.Engine SELECT film.film_id AS film_film_id, ... FROM film WHERE film.film_id = ? ... <sqlalchemy.ext.automap.film object at 0x1013b2810> >>> q.first() # 条件に一致する最初のレコード 2017-10-01 23:29:52,678 INFO sqlalchemy.engine.base.Engine SELECT ... FROM film LIMIT ? OFFSET ? 2017-10-19 23:29:52,678 INFO sqlalchemy.engine.base.Engine (1, 0) <sqlalchemy.ext.automap.film object at 0x1013bce10> >>> q[3] # インデクスでアクセスしても取り出せる ... <sqlalchemy.ext.automap.film object at 0x1013d0250> セッションの中では
 同じレコードは
 同じオブジェクト
  29. 29. DBからオブジェクトを取り出しましょう
 (クエリを実行する) # all() は条件に一致する全てのレコードを返す >>> q.all() ... [<sqlalchemy.ext.automap.film object at 0x1013d00d0>, <sqlalchemy.ext.automap.film object at 0x1013bcf90>, ...] # クエリをスライスすると "LIMIT ... OFFSET ..." >>> q[:10] 2017-10-01 23:36:51,815 INFO sqlalchemy.engine.base.Engine SELECT ... FROM film LIMIT ? OFFSET ? ... [<sqlalchemy.ext.automap.film object at 0x1013d00d0>, ...]
  30. 30. DBからオブジェクトを取り出しましょう
 (クエリ条件を指定する) # filter() に SQL 式を渡せば絞りこめる # filter() は条件の付加されたクエリオブジェクトを返す >>> q2 = q.filter(Film.language_id==1) >>> q2 <sqlalchemy.orm.query.Query object at 0x1013d9690> >>> str(q2) 'SELECT film.film_id ... FROM film WHERE film.language_id = ?' # 一致条件は filter_by でも指定できる >>> q.filter_by(language_id=2).all() 2017-10-01 23:52:27,650 INFO sqlalchemy.engine.base.Engine SELECT ... FROM film WHERE film.language_id = ? # filter, filter_by をつなげて条件を積み上げられる >>> q.filter(Film.rating=='G').filter(Film.title.like('%MOON%')).all() 2017-10-02 00:05:34,056 INFO sqlalchemy.engine.base.Engine SELECT ... FROM film WHERE film.rating = ? AND film.title LIKE ? .... [<sqlalchemy.ext.automap.film object at ...>, ...]
  31. 31. オブジェクトの値にアクセスしましょう
 (オブジェクトの中身) >>> f = q.first # オブジェクトにはテーブルのカラムと同じ名前のアトリビュート がある # language_id だけでなく lanugage というアトリビュートがある # _collection のついた怪しげなアトリビュートもある >>> dir(f) [..., 'description', 'film_actor_collection', 'film_category_collection', 'film_id', 'inventory_collection', 'language', 'language_id', 'last_update', 'length', 'metadata', 'original_language_id', 'prepare', 'rating', 'release_year', 'rental_duration', 'rental_rate', 'replacement_cost', 'special_features', 'title']
  32. 32. オブジェクトの値にアクセスしましょう (カラム値にアクセスする) # カラム名のアトリビュートから値にアクセスできる >>> f.title 'ACADEMY DINOSAUR' # カラムのデータ型に応じて適切なPythonデータ型になる >>> [f.title, f.length, f.rental_rate, f.last_update] ['ACADEMY DINOSAUR', 86, Decimal('0.99'), datetime.datetime(2011, 9, 14, 18, 5, 32)]
  33. 33. inventory_collection オブジェクトの値にアクセスしましょう (外部キー参照にアクセスする) # language は自動マッピングで作られた Language への参照 # 参照すると自動的にクエリを実行する >>> f.language.name 2017-10-02 00:22:06,996 INFO sqlalchemy.engine.base.Engine SELECT language.language_id ... FROM language WHERE language.language_id = ? ... 'English' # *_collection は他のテーブルからの参照の逆参照で、リストを返す >>> f.inventory_collection 2017-10-02 00:11:48,828 INFO sqlalchemy.engine.base.Engine SELECT inventory.inventory_id ... FROM inventory WHERE ? = inventory.film_id ... [<sqlalchemy.ext.automap.inventory object at 0x101510850>, ...] film language film film inventory inventory backref
  34. 34. ORM操作の流れ マッピングを設定する セッションクラスを設定する セッションインスタンスを作る オブジェクトを
 DBから取り出す オブジェクトを クラスから生成する セッションに
 追加する オブジェクトを 更新する セッションをflush/rollback マッピング 関連の操作 セッションの
 操作 セッションを破棄 毎回セッションを 作る時のサイクル セッションを 使い続けるときの サイクル
  35. 35. オブジェクトの値を更新しましょう (アトリビュートの更新) # アトリビュートに代入すると、値をセットできる # まだ SQL は実行されない >>> f.title = 'HOLY GRAIL' >>> f.title 'HOLY GRAIL' # 次のDB操作前にUPDATEが実行される(トランザクション内) >>> q.count() 2017-10-02 00:48:07,739 INFO sqlalchemy.engine.base.Engine UPDATE film SET title=? WHERE film.film_id = ? # セッションを rollback() すると更新はすべて破棄される # ロールバックしたオブジェクトを再度参照するとDBクエリが実行される >>> s.rollback() >>> f.title 2017-10-02 00:51:24,693 INFO sqlalchemy.engine.base.Engine SELECT film.film_id ... 'ACADEMY DINOSAUR' # セッションを flush() すると(トランザクション内で)更新SQLが走る >>> f.title = 'HOLY GRAIL' >>> s.flush() 2017-10-02 06:20:56,690 INFO sqlalchemy.engine.base.Engine UPDATE film SET title=? WHERE film.film_id = ? ... # セッションを commit() するとトランザクションを COMMIT する >>> s.commit() 2017-10-02 06:29:34,104 INFO sqlalchemy.engine.base.Engine COMMIT
  36. 36. オブジェクトの値を更新しましょう (リレーションの更新) # リレーションのアトリビュートに代入すると、値をセットできる # 代入するのはマッピングクラスのインスタンス >>> Language = Base.classes.language >>> lang = session.query(Language).get(2) >>> lang.id, lang.name (2, 'Italian') >>> f.language_id, f.language.language_id, f.name (1, 1, 'English') >>> f.lanugage = lang # 注意: language_id は変更されない! # flush() してはじめて外部キーが更新される >>> f.language_id, f.language.language_id, f.name (1, 2, 'Italian') >>> session.flush() >>> f.language_id, f.language.language_id, f.name (2, 2, 'Italian') #### 大事なこと:リレーションを更新したら必ず flush() する
  37. 37. ORM操作の流れ マッピングを設定する セッションクラスを設定する セッションインスタンスを作る オブジェクトを
 DBから取り出す オブジェクトを クラスから生成する セッションに
 追加する オブジェクトを 更新する セッションをflush/rollback マッピング 関連の操作 セッションの
 操作 セッションを破棄 毎回セッションを 作る時のサイクル セッションを 使い続けるときの サイクル
  38. 38. オブジェクトを新規追加しましょう # カラムの値をキーワードパラメタで渡してオブジェクトを生成する >>> Language(language_id=99, name='Japanese', last_update=datetime.now()) <sqlalchemy.ext.automap.language object at 0x103243f60> >>> l = Language(language_id=99, name='Japanese', last_update=datetime.now()) # セッションに追加すると新規追加対象になる >>> s.add(l) # セッションを flush() すると INSERT 文が実行される >>> s.flush() 2017-10-02 07:12:26,651 INFO sqlalchemy.engine.base.Engine BEGIN (implicit) 2017-10-02 07:12:26,652 INFO sqlalchemy.engine.base.Engine INSERT INTO language (language_id, name, last_update) VALUES (?, ?, ?) 2017-10-02 07:12:26,652 INFO sqlalchemy.engine.base.Engine (99, 'Japanese', '2017-10-02 07:12:12.932744') >>>
  39. 39. オブジェクトを削除しましょう # セッションの delete() メソッドに削除したいレコード(のオブジェクト)を渡す >>> s.delete(l) >>> s.flush() 2017-10-02 07:19:53,458 INFO sqlalchemy.engine.base.Engine SELECT ... FROM film WHERE ? = film.original_language_id 2017-10-02 07:19:53,464 INFO sqlalchemy.engine.base.Engine DELETE FROM language WHERE language.language_id = ? 2017-10-02 07:19:53,464 INFO sqlalchemy.engine.base.Engine (99,)
  40. 40. まとめ • automapを使ってマッピングをDBから生成できる • session.query() でクエリを生成して
 DBからオブジェクトを取り出す • 新しくオブジェクトを追加するときはセッションにadd()
 削除するときは delete() • flush()するか、次にDBと同期する必要が出るまでクエリは実行 されない • セッションを commit() すると、トランザクションが反映される
  41. 41. # ベースクラスを生成する from sqlalchemy.ext.declarative import declarative_base Base = declarative_base() # ベースクラスを拡張するとORMで扱えるモデルクラスになる from sqlalchemy import Column, Integer, String, DateTime 
 class Actor(Base): __tablename__ = 'actor' actor_id = Column(Integer,primary_key=True) first_name = Column(String) last_name = Column(String) last_update = Column(DateTime) def full_name(self): return '{} {}'.format( self.first_name, self.last_name) 宣言的マッピング
  42. 42. # テーブルのスキーマ定義を作成する >>> from sqlalchemy import Table, Column, MetaData, Integer >>> foo_table = Table( ... 'foo', MetaData(), Column('bar', Integer)) # エンティティクラスを作成する >>> class Foo(object): ... def bar_is_odd(self): ... return bool(bar % 2) # マップする >>> from sqlalchemy.orm import mapper >>> mapper(Foo, foo_table) <Mapper at 0x105e332d0; Foo> # dir(Foo) してみましょう。 Foo.bar があることを確認しましょう。 # マッピングに失敗して、やりなおしたら以下のようなメッセージが出る場合: sqlalchemy.exc.ArgumentError: Class '...' already has a primary mapper defined. Use non_primary=True to create a non primary Mapper. clear_mappers() will remove *all* current mappers from all classes. >>> from sqlalchemy.orm import clear_mappers >>> clear_mappers() 古典的マッピング
  43. 43. 以上です。 おつかれさまでした!

×