{ "cells": [ { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "## イミュータブルデータ・モデルを1年使ってみた\n", "\n", "2018-02-09\n", "\n", "[@grimrose](https://twitter.com/grimrose)\n", "\n", "[吉祥寺.pm13](https://kichijojipm.connpass.com/event/75327/)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "### テーマ\n", "\n", "**「新しい挑戦、新しい視点」**\n" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "### お前、誰よ\n", "\n", "* よしだ\n", "\n", " * twitter: [@grimrose](https://twitter.com/grimrose)\n", " * github: [@grimrose](https://github.com/grimrose)\n", "\n", "* とある人材紹介会社のマーケティング部門のデータエンジニア(自称)\n", "* 好きな言語: Groovy\n", "* 好きなIDE: IntelliJ IDEA\n", "* Scala歴: 3年(2015~)\n", "* [ScalaMatsuri](http://scalamatsuri.org) 2016, 2017, 2018 スタッフ" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "### 普段の仕事\n", "\n", "* 事業KPIの可視化を行うためのWebアプリケーションの設計から運用\n", "* Scalaを使って日次や月次の集計バッチを実行するアプリケーションの作成\n", "* 帳票や外部APIなどからデータベースへデータを投入するツールの作成\n", "* 部門の日常業務を改善するためのちょっとしたツールの作成" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "### きっかけ\n", "\n", "

update文、使わなくてもロジック組めるよって分かると、update使わなくなるなぁ

— とーます (@grimrose) 2017年8月30日
\n" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "### 話すこと\n", "\n", "イミュータブルデータ・モデルを1年使ってみてどうだったかを端折って" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "### 参考資料\n", "\n", "[イミュータブルデータモデル(入門編)](https://www.slideshare.net/kawasima/ss-40471672)\n", "\n", "[イミュータブルデータモデル(世代編)](https://www.slideshare.net/kawasima/ss-44958468)\n", "\n", "[データ履歴管理のためのテンポラルデータモデルとReladomoの紹介 #jjug_ccc #ccc_g3](https://www.slideshare.net/itohiro73/jjug-ccc-2017-spring-bitemporal-data-modeling-and-reladomo)\n", "\n", "[イミュータブルデータモデルと webアプリケーションにおける現実解](https://qiita.com/urakawa/items/3d7777e6734cb5c15bd1)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "![image](img/ER.svg)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "### 要件\n", "\n", "* とある業務に関する操作履歴(どのカラムの値をどう変更したか等)を残す\n", "* ある時点での結果で集計を行うが、同月内に複数回人為的に集計と確認のタイミングが発生する\n", " * 前回の集計との比較が行えるようにする\n", " * カラムの値を操作をするので前回との差分も見えるようにする" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "### ORM\n", "\n", "* プロジェクトが始まったのは2016年\n", "* PHPを使うメンバーが多かった\n", " * FuelPHPのアップデートが止まっていた\n", " * 別のフレームワークを選定 -> Laravel\n", "* 集計は、日次バッチを運用しているScalaに" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "#### PHP\n", "\n", "主に登録\n", "\n", "* [Laravel](https://readouble.com/laravel/5.2/ja/) 5.2(とある事情により)\n", " * [クエリービルダー](https://readouble.com/laravel/5.2/ja/queries.html)\n", " * [Eloquent](https://readouble.com/laravel/5.2/ja/eloquent.html)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "#### Scala\n", "\n", "主に集計\n", "\n", "* [Skinny-ORM](http://skinny-framework.org/documentation/orm.html)\n", "* [ScalikeJDBC](http://scalikejdbc.org/)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "### insert\n", "\n", "```php\n", "// イベントの登録\n", "$event = new Event();\n", "...\n", "\n", "$id = DB::table('登録イベントテーブル')->insertGetId($event->as_array());\n", "```\n" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "### update(delete -> insert)\n", "\n", "```php\n", "// 現在のレコードを取得\n", "$record = DB::table('登録イベントテーブル')->where('id', '=', $id)->get();\n", "\n", "// 履歴テーブルへのinsert\n", "DB::table('登録イベント履歴テーブル')->insert($record->as_array());\n", "\n", "// 前イベントIDのレコードを削除\n", "$previousId = $record->id;\n", "DB::table('登録イベントテーブル')->where('id', '=', $previousId)->delete();\n", "\n", "// イベントの登録\n", "$event = new Event();\n", "$event->previousId = $previousId;\n", "...\n", "\n", "$id = DB::table('登録イベントテーブル')->insertGetId($event->as_array());\n", "```\n" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "### ぶっちゃけどうなの?\n", "\n", "* マスタに関してReladomo知っていれば、使っていた\n", " * 2年前は知らなかった\n", " * 有効時間データモデルという名前がついてたのは知らなかった\n" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "### ぶっちゃけどうなの?\n", "\n", "* 前イベントIDを辿る際に再帰クエリ使えないDB(MySQL5.x)だとアプリケーション側で頑張るしか無い\n", " * SQLアンチパターンのナイーブツリー\n", " * 単方向への履歴なのでやむを得ず" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "### ぶっちゃけどうなの?\n", "\n", "ORMについて\n", "\n", "* ORMの機能は殆ど使わなかった(使えなかった)\n", " * 下手にクエリビルダー使うより素直にSQL書いた方が見やすい\n", " * updated_atのカラムが無いので暗黙的更新が邪魔\n", "* Repositoryのinterfaceを提供\n", " * store\n", " * `$storedId = (is_null($entity->id)) ? insert($entity) : update($entity)`" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "### ぶっちゃけどうなの?\n", "\n", "パフォーマンス\n", "\n", "* 書き込みが多い業務だとパフォーマンスに影響がでるかも\n", " * 書き込みは、DBのインスタンスに依存するが、利用頻度が少ないので影響は出ていない。\n", " * 読み込みは、地道にインデックスを貼れば問題ない" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "### おわりに\n", "\n", "データモデルは、要件に合わせて採用しましょう。\n" ] } ], "metadata": { "celltoolbar": "Slideshow", "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.6.4" } }, "nbformat": 4, "nbformat_minor": 2 }