データベースのPRIMARY KEYを自動採番せずにアプリケーション側で生成する

データベースの自動採番

データベースにはPRIMARY KEY(要はID)を自動採番で生成してくれる機能を提供していることが多い。

サロゲートキーのような、特に意味のないデータのIDを発行するときに重宝する。

自動採番のデメリット

しかし、自動採番にはデメリットもある。

IDの発行がRDBMSに依存してしまう

型やSQLも含め、自動採番の機能はRDBMSによって異なることが多いため、アプリケーションのRDBMSへの依存度が高くなってしまう。

DBをまるごと乗せ換えるようなケースはそれほど多くないと思うが、データアクセスのライブラリやフレームワークも含め、バージョン間での互換性は気になるところ。

自動採番されたIDを知るコストがかかる

InsertしたばかりのデータのIDを使い回すケース等で発生しがち。

同一トランザクションで関連テーブルのFKとして登録することはよくあると思う。

ライブラリやフレームワークが解決してくれることも多いが、処理コストは間違いなくかかっているはずだし、場合によっては実装コストもかかってくる。

アプリケーション側で生成するという選択肢

「データベース=データを保存するところ」と考えると、「アプリケーション=データを作るところ」と考えられると思う。

であれば、サロゲートキーのようなものであっても、データの一意性はまず作成側で担保するべきともいえるし、作成側が最初から知っているべき情報ともいえる。

Javaで生成する例

JavaのUUIDを利用して擬似乱数を生成し、それを文字列化してIDにする。

UUID (Java Platform SE 8)

String id = UUID.randomUUID().toString();

UUID#toString() は常に36文字になるため、DB側は CHAR(36) で宣言するとよい。

補足

順序性

IDに順序性が求められる場合もあると思うが、ソートが要件に入っている時点で意味を持っているカラムと考えられる。

IDと順序性は同列に語るものではない気がする。

「ID順」が守るべきソート順は何か?ということなので、たとえば登録順なら登録日時として持つべきだよね、とかそういう話だと思う。

衝突

乱数生成の場合、理論上衝突する可能性がある。

一概にはいえないが、データの整合性を(最終的に)守ることはデータベース側に任せて良いのではないかと思う。

要はDBの制約に頼ろうという話で、エラーハンドリングであったりリトライで解決できるレベルの話ではなかろうか。

頻度にもよるとは思うが、固く行くなら重複チェック処理を混ぜ込むか。
(その場合コストは自動採番と同様になるが、RDBへの依存度は下がる)

参考

stackoverflow.com

MyBatisのuseGeneratedKeysがPostgreSQL10でエラー #MyBatis

MyBatisにはRDBが自動採番したIDを取得する機能があるのだが、下記の組み合わせでエラーになってしまった。

### Error updating database.  Cause: org.postgresql.util.PSQLException: 自動生成キーの戻りは 8.2 以上でサポートされます。

余裕で8.2以上ではあるものの、PostgreSQLを9にしてみるとうまく動いたのであった。

PostgreSQLのDocker公式イメージを使って開発用DBを構築する #PostgreSQL

前提

  • Docker for Mac使用
  • クライアントにpostgresqlを導入済み
    • ホスト側からpsql等で接続するため

コンテナの起動

  • イメージは postgres:10
  • コンテナ名は dev-postgres
  • パスワードは postgres
  • ポートは 15432 (コンテナの5432番にフォワードする)
$ docker run --name dev-postgres -e POSTGRES_PASSWORD=postgres -p 15432:5432 -d postgres:10

psqlでの接続

  • postgresユーザで接続
$ psql -h localhost -p 15432 -U postgres

ユーザの作成

  • postgresユーザで作成
  • パスワードを設定する
  • ユーザ名は sample_owner
$ createuser sample_owner -h localhost -p 15432 -P -U postgres

createuser

アプリケーション用ユーザの作成

  • postgresユーザで作成
  • ユーザ名は sample_app_user
$ createuser sample_app_user -h localhost -p 15432 -P -U postgres

データベースの作成

  • postgresユーザで作成
  • Ownerは sample_owner
  • データベース名は sample
$ createdb -O sample_owner -h localhost -p 15432 -U postgres sample 'サンプルのデータベース'

下記のSQLを発行したのと同じ扱い

CREATE DATABASE sample OWNER sample_owner;
COMMENT ON DATABASE sample IS 'サンプルのデータベース';
$ psql -h localhost -p 15432 -U postgres sample

createdb

スキーマの作成

5.8. スキーマ

権限の設定

PostgreSQLではユーザーを作成するとデータベースへの接続権限とPublicスキーマへの権限がデフォルトで付与されている。

なので、まず権限を剥がして、その後必要な権限を付与するという形にする。

  • デフォルトの権限を剥がしておく
  • publicスキーマの権限は、sampleデータベースに接続の上で剥がす
REVOKE CONNECT ON DATABASE postgres,template0,template1,sample FROM PUBLIC;
REVOKE ALL ON SCHEMA public FROM PUBLIC;
  • postgresユーザでデータベースとスキーマへの権限を付与する
-- データベースの操作権限
GRANT ALL ON DATABASE sample TO sample_owner;
-- スキーマへの操作権限
GRANT ALL ON SCHEMA public TO sample_owner;
GRANT USAGE ON SCHEMA public TO sample_app_user;
  • sample_ownerユーザでテーブルへの権限を付与する
  • sample_app_userにはCRUDのみ許可する
-- データベースへの接続権限
GRANT CONNECT ON DATABASE sample TO sample_app_user;
-- テーブルへの操作権限
GRANT INSERT,SELECT,UPDATE,DELETE ON ALL TABLES IN SCHEMA public TO sample_app_user;

確認

  • sample_ownerでログインできる
  • sample_ownerはCREATE TABLEできる
  • sample_userでログインできる
  • sample_userでSELECTできる
  • sample_userでCREATE TABLEできない
$ psql -h localhost -p 15432 -U sample_owner sample
$ psql -h localhost -p 15432 -U sample_app_user sample

参考

PostgreSQL 10.0文書 kimulla.hatenablog.com qiita.com qiita.com

Nuxt.jsでWebSocketを使ってみる #nuxtjs #vuejs

Nuxt.jsでWebSocketを使ってみる。

バックエンドはexpress-templateを使ってExpressで用意する。(Nuxt.jsはWebSocketのクライアントとして使う)

su-kun1899.hatenablog.com

サーバ側

nuxt-expressテンプレートを使っていることを前提とする。

ExpressでWebSocketを使えるように、 express-ws を追加する。

$ npm install --save express-ws

WebSocket用のエンドポイントを用意する。

server/index.js を修正。

import express from 'express'
import { Nuxt, Builder } from 'nuxt'
// expressWsを使う
import expressWs from 'express-ws'

const app = express()
const host = process.env.HOST || '127.0.0.1'
const port = process.env.PORT || 3000

// WebSocket用のエンドポイントを追加
expressWs(app)
app.ws('/ws', function(ws, req) {
  ws.on('message', function(msg) {
    console.log('from server: ' + msg)
  })
})

// Nuxtのビルド周りは省略

// 起動
app.listen(port, host)
console.log('Server listening on ' + host + ':' + port)

クライアント側

クライアント側はWebSocket-Nodeを使ってみた。

$ npm install --save websocket

実際にやりとりするpageとして pages/ws/index.vue を作成。

画面を表示するとWebSocket接続、ボタン押下でメッセージを送信するようにしている。

<template>
  <div>
    <textarea v-model="message"></textarea>
    <br/>
    <button v-on:click="send">送信</button>
    <p>You recieved message: <b>{{answer}}</b></p>
  </div>
</template>

<script>
import { w3cwebsocket } from 'websocket'
const W3CWebSocket = w3cwebsocket

export default {
  data: function() {
    return {
      // WebSocketクライアントの生成
      socket: new W3CWebSocket('ws://localhost:3000/ws'),
      // クライアントから送る値(textarea)
      message: '',
      // サーバから受け取る値
      answer: ''
    }
  },
  // createdライフサイクルで、Vueインスタンスが作成されたら
  // イベントリスナーを登録しておく
  created: function() {
    // イベントリスナーからVueコンポーネントに値を渡すために一度selfで変数化しておく
    const self = this
    self.socket.onmessage = function(e) {
      if (typeof e.data === 'string') {
        self.answer = e.data
      }
    }
  },
  methods: {
    send: function() {
      // ボタン押下でサーバに値を送る
      this.socket.send(this.message)
    }
  }
}
</script>

参考

github.com github.com github.com qiita.com qiita.com

jestでVuexのmapMutationsがエラーになる #vuejs #nuxtjs #jest

jestでVuexのmapMutationが Unexpected token on mapMutations なエラーになってしまう。

...mapMutations... がbabelによってエラーになってしまっているようだ。

... はスプレッド演算子(object rest spread operator)というらしい。

解決方法

スプレッド演算子をbabelがテスト時もトランスパイルできるように babel-plugin-transform-object-rest-spread を追加する。

$ npm install --save-dev babel-plugin-transform-object-rest-spread

.babelrc に設定を追加する。

{
  //...
  "env": {
    //...
    "test": {
      "plugins": ["transform-object-rest-spread"]
    }
  }
  //...
}

参考

jestでNuxt.jsの依存コンポーネントのパスがエラー #nuxtjs #vuejs #jest

Nuxt.jsはsrcディレクトリやrootディレクトリにエイリアスがついており、importなどではそれを使用する。

こんな感じで。

import child from '@/components/Child'

しかしjestでテストを書こうとした場合に、パスが解決出来なくてエラーになってしまう。

Cannot find module '../pages/index.vue' from 'index.test.js'

解決策

package.jsonで、jestの設定に moduleNameMapper を追加する。

これでパスを意図したとおりに解決してくれる様になる。

"moduleNameMapper": {
  "^@/(.*)$": "<rootDir>/$1",
  "^~/(.*)$": "<rootDir>/$1"
}

shallowを使う

UnitTestであればそもそもサブコンポーネントの依存を切り離してテストをした方がよい。

vue-test-utilsを使っているのであれば、shallowを使うとサブコンポーネントはスタビングされる。

import { shallow } from 'vue-test-utils'
import index from '../pages/index.vue'

describe('shallow sample', () => {
  test('shallowを使ってテスト', () => {
    // shallowの場合はサブコンポーネントがレンダリングされない
    const wrapper = shallow(index)

    expect(wrapper.vm.message).toEqual('Hello, shallow!')
  })
})

参考

期間の範囲を示すパラメータ名について考えてみた #programming

概要

APIのパラメータとして期間の範囲を表す命名について、職場でいい議論がなされたので自分なりにまとめておく。

期間の範囲というのは「開始日時」「終了日時」のようなものを想定。

start-end

扱いやすそうなのはこれ。

日時の場合は、終了側が排他的であった方が扱いやすいことが多い。 ※一ヶ月分取得しようと考えた際に、末日が月によって変わってしまうので、1/1-2/1 と検索可能なほうがうれしい

endは排他の命名としてリーダブルコードでも紹介されている。

ただしリーダブルコードでは begin-end で紹介している。

startとbeginは基本的には置き換え可能らしいが、 startの対義はstopであるのが正しいようだ。

しかし開始時間は start time のように表現することが多いので悩ましいところ。

Kotlinの TimeRanges が使っている。

startInclusive-endExclusive

開始が包含で、終了が排他であることが一目瞭然。

分かりやすさが重視されており、なかなかよい。

endは排他なのは前述の通りなのだが、英語が母語でない人に配慮したのかなと想像している。

しかし「長い/冗長」という意見もあった。(Docにちょっと書いておくくらいでいいのかも、という声も)

Javaで時間の範囲を扱うクラスの引数は上記のような命名になっている。

since-until

ほぼ間違いなく時間を指すだろうことを連想できる。

unitlも排他になる。

しかしsinceはどうやら未来のことを表現するのに相性がよくないようなので、性質をよく考慮したほうがいいかもしれない。
(since には「ご存知のように」的なニュアンスが含まれるらしい)

Goの time.Duration や、 RailsActiveSupportDuration はsince-untilのようだ。

from-to

from-toの場合は、時間軸には向いていないかもしれない。

from Sender to Reciever のように、時間以外の表現の場合は有力な選択肢になりうる。

ちなみにGoogleカレンダーの予定の入力欄を英語版で見たところ from-until でした。

offset-limit

limitは包含のニュアンスになるので、日時を扱う場合は微妙になる。

ただSQL等で使われる馴染みのキーワードであるので、包含させたい場合は有用。

first-last

こちらもlastは包含される。

min-max

maxは包含される。

あまり時間には使わなそう。

RubyTimeComparable のbetweenを使ってるっぽいので、 min-maxってことになるのかな。