Slide 1

Slide 1 text

アンチパターン対策会議!!!! 作成者:えばた あや 2022/12/05

Slide 2

Slide 2 text

自己紹介 株式会社Alumnote えばた あや ◆プロフィール ● 株式会社AlumnoteでバックエンドをGoで書いています! ● ラーメン二郎が好きです!

Slide 3

Slide 3 text

会社紹介 国内大学・教育機関の財政難を解決するために、 大学の財務構造(資金調達手段)のアップデートを目指しているスタートアップです。 大学支援者ネットワークのデータベース化〜拡大活性化、寄付募集の最大化を実現する Vertical SaaSの開発・提供と大学実務のハンズオン支援を行っています。 エンジニアメンバーがまだ少ないため、 ごーふぁー荘にて社内勉強会を開催することになりました!

Slide 4

Slide 4 text

こんな感じのサービス作ってます CRMツール 決済基盤 ・エクセルなど複数のシステムに分散  している卒業生・寄付者名簿を  名寄せ・統合して管理 ・名簿情報をさまざまな項目で簡単に  フィルタリングして抽出 ・HTMLメールをパソコン画面上で  簡単に作成し、名簿から配信可能 ・配信したメールの到達率、開封率など  の分析機能も標準で提供 ・寄付決済、会費決済向けの様々な  決済手段をワンストップで提供 ・決済情報は名簿情報と自動的に連携 大学支援者・関係者の名寄せ〜名簿管理 寄付マーケティングに必要なツール群 寄付者のデータが名簿に 自動連携される決済ツール コミュニティ ・卒業生検索・キャリアサービスなど  在校生や卒業生への便益提供が可能 ・名簿情報と自動連携されており、  最新の個人情報に更新できる 在校生・卒業生ネットワークの アクティベーション

Slide 5

Slide 5 text

目次 2 gormアンチパターン 3 SQLアンチパターン 4 まとめ 1 背景

Slide 6

Slide 6 text

今日は、 みんなとディスカッションしながら 登壇できたらいいなって思ってます! コアなGoエンジニアが少ないと Goな話できなくて寂しいのじゃ

Slide 7

Slide 7 text

レビューの時に、GoのライブラリやDBの使い方の勘違いを発見!!><   ↓ 今後も同じミスを起こさないようにするために振り返り&改善をしていきたい!!! ※ 今回はPostgreSQLとgorm v2使ってます 背景

Slide 8

Slide 8 text

まずはgromのアンチパターンから!

Slide 9

Slide 9 text

以下のソースコードの問題はなんだろう? 前提: 以下に該当するレコードが1件インサート済み gormアンチパターン type Organization struct { Name string StartYear int } db.Where("id = ?", oid).Updates(Organization{ Name: "ほげ", StartYear: 0, })

Slide 10

Slide 10 text

答え: Updatesはゼロ値を更新しないのでStartYearが更新されない gormアンチパターン type Organization struct { Name string StartYear int } db.Where("id = ?", oid).Updates(Organization{ Name: "ほげ", StartYear: 0, })

Slide 11

Slide 11 text

答え: Updatesはゼロ値を更新しないのでStartYearが更新されない ゼロ値とは 変数を宣言してその後値を代入しなかった時に入ってるデフォルトの値 例: gormアンチパターン UPDATE "organizations" SET "name"='ほげ' WHERE id = '12345' var i int // intのゼロ値は0 var s string // stringのゼロ値は"" fmt.Printf("i: %d, s: %s", i, s) // 「i: 0, s: 」が出力される

Slide 12

Slide 12 text

対策: Updatesを使う場合はmapかSelectを使って書く必要がある! gormアンチパターン type Organization struct { Name string StartYear int } db.Model(&Organization{}). Where("id = ?", oid). Updates(map[string]interface{}{ "name": "ほげ", "start_year": 0, })

Slide 13

Slide 13 text

対策: Updatesを使う場合はmapかSelectを使って書く必要がある! gormアンチパターン type Organization struct { Name string StartYear int } db.Select("name", "start_year"). Where("id = ?", oid). Updates(Organization{ Name: "ほげ", StartYear: 0, })

Slide 14

Slide 14 text

以下のソースコードの問題はなんだろう? 前提: 以下に該当するレコードが1件インサート済み gormアンチパターン type Organization struct { ID uuid.UUID Name string StartYear int } o := Organization{ ID: oid, Name: "ほげ", } if o.ID == uuid.Nil { // エラー処理 } db.Save(&o)

Slide 15

Slide 15 text

答え: Updatesメソッドと違ってSaveメソッドは渡されなかったものはゼロ値で更新される gormアンチパターン type Organization struct { ID uuid.UUID Name string StartYear int } o := Organization{ ID: oid, Name: "ほげ", } if o.ID == uuid.Nil { // エラー処理 } db.Save(&o)

Slide 16

Slide 16 text

答え: Updatesメソッドと違ってSaveメソッドは渡されなかったものはゼロ値で更新される gormアンチパターン UPDATE "organizations" SET "name"='ほげ',"start_year"=0 WHERE "id" = '12345'

Slide 17

Slide 17 text

対策: Firstメソッドで1件該当するレコードを取得してからその値を使って更新する gormアンチパターン type Organization struct { ID uuid.UUID Name string StartYear int } var o Organization db.Where("id = ?", oid).First(&o) o.Name = "ほげ" db.Save(&o)

Slide 18

Slide 18 text

最終的にAlumnoteでのgormの書き方は… 各ユースケースに合わせたメソッドというよりも、汎用的に更新するメソッドとして作っているので UpdatesメソッドではなくてSaveメソッドを用いて実装した - どのカラムを更新するかはユースケース層で各ユースケースに合わせて実装 - Updatesメソッドは特定のカラムを更新するために使うイメージだったため、Saveメソッドを 利用 - それによりUpdatesで引っかかりがちなところを避けて行けるようになった! - SaveメソッドはIDがない場合はinsertになってしまうけど、現状気にするのはここだけ になった もっといい方法あるよとか、こんなアンチパターンもあるよ、とかあれば教えてください!!w gormアンチパターン

Slide 19

Slide 19 text

つぎはSQLのアンチパターン!!!

Slide 20

Slide 20 text

やりたかったこと - 似ている人の組み合わせをDBに保存して、関連する似ているユーザを一塊にして返す - pagenationしたい 例: - AさんはBさんと似ている - AさんはCさんと似ている - CさんはDさんと似ている - EさんはFさんと似ている SQLアンチパターン

Slide 21

Slide 21 text

元のデータの持ち方 imported_user_id: インポートしたユーザのID original_user_id: インポートしたユーザに似たユーザのID   imported_user_diffsテーブル SQLアンチパターン ID imported_user_id original_user_id 1 B A 2 C A 3 D C 4 F E

Slide 22

Slide 22 text

APIで出力したい形 { data: [ { ids: [“1”, “2”, “3”], // imported_user_diffsテーブルのid(後で使うから一応渡しておく data: [ // 似ているユーザの一塊を配列にしたもの {id: “A”(ユーザの情報も一緒に返す)}, {id: “B”(ユーザの情報も一緒に返す)}, {id: “C”(ユーザの情報も一緒に返す)}, {id: “D”(ユーザの情報も一緒に返す)}, ], }, { ids: [“4”], data: [ {id: “E”(ユーザの情報も一緒に返す)}, {id: “F”(ユーザの情報も一緒に返す)}, ], }, ], total: 2, } SQLアンチパターン

Slide 23

Slide 23 text

この設計のデメリット - 似ているユーザを一塊にするSQLが一筋縄で書けない - 厳密に言うとPostgreSQLだと再帰ができるので書けるけど…スマートとは思えない - pagenationができない SQLアンチパターン

Slide 24

Slide 24 text

この設計のデメリット - 似ているユーザを一塊にするSQLが一筋縄で書けない - 厳密に言うとPostgreSQLだと再帰ができるので書けるけど…スマートとは思えない - pagenationができない 調べてみたらアンチパターンと言うことが判明した!!! SQLアンチパターン

Slide 25

Slide 25 text

ナイーブツリー 自分自身と自分自身の親を持つことで木構造を表現するアンチパターン 今回の例で言うとこういう木構造になる SQLアンチパターン A B C D E F

Slide 26

Slide 26 text

ナイーブツリーのデメリット - 親であるAの子供一覧を取得しにくい - 末端のDから親を辿るのが難しい - Cが削除されるとDの繋がりがなくなる…(Aが親だと言う事実は消したくない SQLアンチパターン

Slide 27

Slide 27 text

今回Alumnoteではこんな感じに対応しました SQLアンチパターン ID imported_user_id original_user_id 1 B A 2 C A 3 D A 4 F E

Slide 28

Slide 28 text

今回Alumnoteではこんな感じに対応しました - original_user_idの値を木構造で言う1つ上のノードの値ではなくて、親の値を持つようにした - 閉包テーブルに近い設計にした - imported_user_id = original_user_idの値は持たなかった - 本来閉包テーブルにはimported_user_id = original_user_idの値を含めるが、もしユー ザを追加したときにimported_user_diffsテーブルにimported_user_id = original_user_idの値を追加するの忘れるとデータがおかしくなる - なくても表現できたのでimported_user_id = original_user_idの値はない方向にした SQLアンチパターン

Slide 29

Slide 29 text

それにより以下が改善 - original_user_idで絞って検索することで簡単に似ているユーザを一塊にすることができるよ うになった! - ユーザの削除があっても関係性が崩れなくなった! - original_user_idでGROUP BYすることで、pagenationもできるようになった! SQLアンチパターン

Slide 30

Slide 30 text

まとめ gormアンチパターンというよりも使い方間違えって感じだったけど、書き方を間違えるとデータに すごい影響があるということに気付けた! SQLアンチパターンは実際設計の時点で知識が足りなくてアンチパターンになってしまったが、それ によってナイーブツリーを知ることができた!

Slide 31

Slide 31 text

募集中! Alumnoteでは正社員・業務委託として一緒に働いて頂ける方を募集中! 応募お待ちしてます! バックエンド https://herp.careers/v1/alumnote/PVuMatXQrdmn フロントエンド https://herp.careers/v1/alumnote/N8ujo5TyyP8h