SQLの練習ができる学習サービス「SQLab」を作ってみた

# はじめに
![sqlab@2x-min.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1074872/29cb6552-8271-3b43-0175-18ba4ae7f161.png)
こんにちは、[タケ](https://twitter.com/take_paolo)と申します。
SQLの練習ができる学習サービス「SQLab」を作りました。

https://sqlab.net/

※ PC専用です。
※ 推奨ブラウザはGoogle Chromeです。

# なぜ作ったの？
SQLを学び始めた当初は **「SQLってなんか難しそう」** というイメージでしたが、
練習問題を解いていく中で次第に自力でクエリを組み立てることが **「クイズみたいで楽しい」** と思うようになりました。
そこでもっと多くの練習問題を解いてみたい、もう少し難易度の高い練習問題を解きたいと思ったのですが、

・無料でバリエーション豊かな多くの練習問題に取り組める
・環境構築不要でブラウザ上ですぐに実践できる
・シンプルで使いやすいUI
・ER図やデータベース値を参考にしながら自力でクエリを組み立てる必要がある

の条件を揃えた学習サービスがなかったので作ることにしました。

# どんな練習問題があるの？
現在は5つの問題集があります。
| 問題集 | 説明 |
| :- | :- |
|  SQLトライアル編  |  初めてご利用される方向け  |
|  SQL初級編  |  SELECT文、WHERE句、算術演算子、比較演算子  |
|  SQL中級編  |  集約関数、データのグループ化・並び替え、データの登録・更新・削除、サブクエリ、JOIN句  |
|  SQL上級編  |  準備中です  |
|  SQLチャレンジ編  |  SQLの基礎力を試したい方向け  |

SQLチャレンジ編では以下のような問題に挑戦できます。

**「店舗毎の書籍の売上を取得してください。その際、店舗名は昇順に並び替えてください。」**
**「複数のカテゴリーに属している書籍名の一覧を取得してください。」**

# こんな方におすすめ
・とにかく多くの問題を解いてSQLのスキルを高めたい方
・有料の学習サービスを利用したが、もう少し難易度の高い練習問題を解いてみたい方
・SQLの技術書を読んだけど次に何をすれば良いかわからない方
・実際にSQLを書いてみたいけど環境構築やデータの用意が面倒だなと感じる方

# 使い方
1. お好きな問題集を選択します
2. 解きたい練習問題を選択します
3. 問題文を読み、クエリをエディタに記述します
4. 答え合わせボタンで正誤判定を行います

正解するまで3〜4を繰り返しましょう！
![demo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1074872/7b34b8d1-d661-3622-2711-532de00f824f.gif)

# 工夫した点
UI/UX向上のために以下のことを工夫しました。

| タブアイテムのオートフォーカス機能 | 実行結果タブへのオートスライド機能 |
| :- | :- |
| ![auto_focus.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1074872/11338063-c0a9-c9f6-67e4-ae6e32064b75.gif) | ![auto_slide.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1074872/b083629d-e43d-eee1-54e2-7c43bb96e946.gif) |
| タブが切り替わると自動的にタブアイテムをフォーカスします。 | ユーザーが「実行」または「答え合わせ」ボタンを押した時に「実行結果」タブへ自動的にスライドします。 |

| ショートカット機能 | 演習画面の設定機能 |
| :- | :- |
| ![shortcut.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1074872/ac840f7c-5574-4f91-b326-d8d0f0dd0f5d.png) | ![settings.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1074872/f18ad2eb-faf9-dc38-88dd-993ce51d81bc.gif) |
| タブの切り替えやクエリ実行、解答例の表示などをコマンド一つで実行できます。 | 実行結果タブへのオートスライド機能やショートカット機能のオンオフを切り替えることができます。 |

# その他機能

| 練習問題のお気に入り登録機能 | ログイン機能 |
| :- | :- |
| ![favorite.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1074872/b206dc7e-3644-c0b1-5920-9f36d51d4824.gif) | ![login-min.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1074872/a9f0fa02-22d5-a013-ef62-91f6f8b2de38.png) |
| ログインユーザーは練習問題をお気に入り登録することができます。お気に入り登録一覧はマイページで確認することができます。 | GoogleまたはGitHubアカウントを用いてアカウント登録・ログインをすることができます。 |

| マイページ機能 | 管理画面機能 |
| :- | :- |
| ![mypage.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1074872/90b7b05f-daa3-47b0-c287-bb6f03a28de3.gif) | ![admin_works.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1074872/702d7212-edb8-85c4-edb2-d93e786295f3.gif) |
| マイページでアカウント登録情報の詳細や更新ができます。<br>また、お気に入り登録した練習問題の確認やお気に入り登録の解除をすることができます。 | 問題集・チャプター・練習問題を管理画面上で作成・更新・削除することができます。<br>また、ドラッグ&ドロップで並び順を任意のタイミングで入れ替えることができます。 |

# 実装時に苦労した点
#### データ取得(SELECT文)だけでなく、データ変更(INSERT・UPDATE・DELETE文)に関する練習問題にも取り組めるようにしたこと
データの整合性をどう保つかが課題でした。
結論としてはリクエストがある度に一時テーブルを作成して、そのテーブルに対してデータ変更を行うようにしました。
一時テーブルのレコード数も10行以下を想定していたので動作が遅くなるなどの影響は少ないと判断しました。(実行ログから一時テーブルの作成時間は6ms~8msでした。)
Dockerなどを用いて、ユーザー毎に仮想環境を提供するという手もありましたが、
SQLのみを題材にしているサービスでそこまでする必要はあるのかという点と
ランニングコストが必要以上にかかってしまうという点で懸念があり、上記で対応しました。

#### ドラッグ&ドロップで問題集・チャプター・練習問題を並び替える機能

```
問題集A
  チャプターA
    練習問題1
    練習問題2
  チャプターB
    練習問題3
    練習問題4
```
[Vue.Draggable](https://github.com/SortableJS/Vue.Draggable)を使用してドラッグ&ドロップ機能を実装しました。
上記のようにリストは階層構造になっており、以下の2つが課題でした。
1. 問題集・チャプター・練習問題毎に並び替えた状態をどう保持するか
2. 並び替えた状態の整合性をどう保つか

1に関しては
問題集・チャプター・練習問題の各リストアイテムにデータ属性(tableType)を設定することで対応しました。
ドラッグ&ドロップ時に実行される以下のメソッド内でデータ属性を取得することでソート結果の格納先を動的に変更しています。
```
onSort(evt) {
  const table = evt.item.dataset.tableType
  const newSortedIds = this.getNodesIds(evt.to.childNodes)
  this.changeSortResult(newSortedIds, this.references[table], this.sortResults[table])
}
```

2に関しては、
元の並び順との比較結果とソート結果が既に存在するかによって
ソート結果の入れ替え・追加・削除を行い、整合性を保つようにしました。
```
changeSortResult(ids, references, sortResults) {
  const reference = references.find(el => intersectionBy(el, ids).length)
  const index = sortResults.findIndex(el => intersectionBy(el, ids).length)

  if (JSON.stringify(reference) !== JSON.stringify(ids)) {
    index !== -1 ? sortResults.splice(index, 1, ids) : sortResults.push(ids)
  } else {
    if (index !== -1) sortResults.splice(index, 1)
  }
}
```

# 使用技術
### フロントエンド
- HTML
- CSS
- SCSS
- JavaScript
- Vue.js (2.6.14)
- Vuetify (2.6.7)

### バックエンド
- Ruby (3.0.2)
- Rails (6.1.4.1)

### コード解析ツール
- ESLint
- Rubocop

### コードフォーマッター
- Prettier

### テスト
- Rspec

### インフラ
- Heroku
- PostgreSQL

# ER図
![er-diagram-min.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1074872/f1a4250c-ec30-7f70-a020-485c9f43e6a8.png)

# 今後の予定
- 練習問題数を80問まで増やす(現在43問)
- マイページ機能の拡張(学習状況の表示機能など)
- デプロイ先の変更(Herokuが無料プランを廃止するため)

# 参考記事

| 記事 | コメント |
|:-|:-|
| [Active Record で複数のデータベース利用](https://railsguides.jp/active_record_multiple_databases.html) | 1つのアプリ内で複数のDBを運用する方法が書いてあります。非常にお世話になりました。 |
| [CodeMirror](https://codemirror.net/) | 演習画面のコードエディタはCodeMirrorを使用しました。テーマも数多くあり、カスタマイズも簡単にできます。 |

# 終わりに
最後まで読んでいただき、ありがとうございました。
ぜひSQLabを使って、SQLの練習問題を解いてみてください。
Twitterもやっていますので、「こういう練習問題を追加してほしい。」などのご要望を頂けますと嬉しいです。

https://sqlab.net

【Twitterアカウント】
https://twitter.com/take_paolo