SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

DeveloperZine(デベロッパージン)- エンジニアの意思決定を支える技術情報メディア ProductZine

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

CoffeeScriptによるモダンなWebアプリケーション開発

CoffeeScriptベストプラクティス集
Node.jsアプリケーション編(3)

CoffeeScriptによるモダンなWebアプリケーション開発 第5回

モデルのデータベースへの反映

 定義したモデルをデータベースに反映させるにはsequelize.sync()を呼びます。sequelize.sync()はデータベースを新しく作成するときや、モデルを変更した時にだけ呼びます。モデルに変更がない限りsequelize.sync()を再度呼ぶ必要はありません。

sequelize.sync()

 sequelize.sync()を実行した時点ですでにデータベースに存在しているテーブルについては再作成されません。ただし、{ force: true }を引数として渡すと、既存のテーブルも削除して新たにテーブルを作成し直します。

sequelize.sync force: true

カラムの詳細オプション

 モデルを定義するとき、カラムの型指定に詳細なオプションを指定できます。詳細なオプションは、カラムごとにオブジェクトで指定します。

[リスト2]モデルに詳細な型を定義する
Person = sequelize.define 'Person',
  firstName:   { type: Sequelize.STRING, allowNull: false }
  lastName:    { type: Sequelize.STRING, allowNull: false }
  dateOfBirth: Sequelize.DATE
  employeeId:  { type: Sequelize.INTEGER, unique: true }

 指定できるオプションは次の通りです。大文字はデフォルト値を表します。

  • allowNull(TRUE/false):値としてNULLを許可するか。
  • defaultValue(値):デフォルト値。
  • unique(true/FALSE):ユニークキー制約を設定するか。
  • primaryKey(true/FALSE):プライマリキーとして使用するか。
  • autoIncrement(true/FALSE):オートインクリメントを使用するか。

 モデルのどのカラムにもprimaryKeyオプションを指定していない場合は、idというプライマリキーのカラムが自動的に追加されます。

インスタンスとレコードの作成

 データベースにレコードを挿入するには、モデルのbuild()メソッドを呼んでインスタンスを作成し、save()を実行します。

[リスト3]モデルのインスタンスを作成してデータベースに保存する
person = Person.build
  firstName:   "賢治"
  lastName:    "宮沢"
  dateOfBirth: new Date 1896, 7, 27

person.save().success ->
  console.log "保存成功"

 保存に成功すると、success()に渡したコールバック関数が呼ばれます。

 カラムの値はインスタンスのプロパティとして参照できます。

person.firstName    # 賢治
person.lastName     # 宮沢
person.dateOfBirth  # Wed, 26 Aug 1896 15:00:00 GMT

レコードの検索

 モデルのfind()メソッドを呼ぶとレコードをインスタンスとして取得します(リスト4)。

[リスト4]IDが1のレコードをインスタンスとして取得する
# SELECT * FROM `Persons` WHERE `id`=1 LIMIT 1;
Person.find(1).success (person) ->
  # クエリが完了するとこの部分が実行される。見つからなかった場合はpersonがnullとなる。
  console.log "id: #{person.id}"
  console.log "firstName: #{person.firstName}"
  console.log "lastName: #{person.lastName}"
  console.log "dateOfBirth: #{person.dateOfBirth}"

 find()メソッドの引数で様々な検索条件を指定できます(リスト5)。複数のレコードが検索条件にマッチした場合は、最初にマッチしたレコードだけを取得します。

[リスト5]検索条件を指定して1つのレコードを取得する
# lastNameが「宮沢」のレコードを探し、最初に見つかったものを取得する。
# SELECT * FROM `Persons` WHERE `lastName`='宮沢' LIMIT 1;
Person.find({ where:{ lastName:'宮沢' } }).success (person) ->

# firstNameが「賢治」かつlastNameが「宮沢」のレコードを探し、最初に見つかったものを取得する。
# SELECT * FROM `Persons` WHERE `firstName`='賢治' AND `lastName`='宮沢' LIMIT 1;
Person.find({ where:{ firstName:'賢治', lastName:'宮沢' } }).success (person) ->

# lastNameが「宮」で始まるレコードを探し、最初に見つかったものを取得する。
# SELECT * FROM `Persons` WHERE lastName LIKE '宮%' LIMIT 1;
Person.find({ where:[ 'lastName LIKE ?', '宮%' ] }).success (person) ->

# firstNameが「りえ」またはlastNameが「宮沢」のレコードを探し、最初に見つかったものを取得する。
# SELECT * FROM `Persons` WHERE firstName = 'りえ' OR lastName = '宮沢' LIMIT 1;
Person.find({ where: ["firstName = ? OR lastName = ?", 'りえ', '宮沢'] }).success (person) ->

 findAll()を呼ぶと、検索条件にマッチしたすべてのレコードをインスタンスに変換し、配列として取得します(リスト6)。

[リスト6]検索結果をすべて取得する
# Personのすべてのレコードを配列で取得する。
# SELECT * FROM `Persons`;
Person.findAll().success (persons) ->
  console.log persons.length

# Personのレコードのうちidが1,2,3のいずれかであるものをすべて取得する。
# SELECT * FROM `Persons` WHERE `id` IN (1,2,3);
Person.findAll({ where:{ id:[1,2,3] } }).success (persons) ->

# Personのすべてのレコードをidの降順でソートして取得する。
# SELECT * FROM `Persons` ORDER BY id DESC;
Person.findAll({ order:'id DESC' }).success (persons) ->

# Personの3番目から7番目までの5個のレコードを取得する。
# SELECT * FROM `Persons` LIMIT 2, 5;
Person.findAll({ offset: 2, limit: 5 }).success (persons) ->

# Personのレコード数を取得する。
# count()の引数にもfind()と同様の条件指定が可能。
# SELECT count(*) as `count` FROM `Persons`;
Person.count().success (count) ->
  console.log "#{count}人"

レコードの削除

 レコードを削除するにはインスタンスのdestroy()メソッドを呼びます。

person.destroy()

エラー処理

 指定されたカラムが見つからない場合やSQLの構文に問題がある場合など、データベースがエラーを返したときはerror()のコールバックが実行されます。この場合success()のコールバックは実行されません。

[リスト7]データベースのエラーを受け取る
Person.find({ where: middleName: 'John' }).success (person) ->
  console.log person.firstName
.error (error) ->
  console.log "db error: #{error}"

 MySQLのテーブルにmiddleNameというカラムが存在しない状態でリスト7を実行すると、次のように出力されます。

db error: Error: Unknown column 'middleName' in 'where clause'

 error()を呼ばないままデータベース側でエラーが起きると、success()のコールバックは呼ばれず、エラーを受け取ることもできません。成功することを見越してエラーを無視することも可能ですが、万が一エラーが起きた時にアプリケーションが止まらないように、最低限のエラー処理は入れておくようにしましょう。

モデルメソッドの定義

 モデルの定義と同時にクラスメソッドやインスタンスメソッドを定義できます。それぞれリスト8のようにdefine()の第3引数で指定します。定義したクラスメソッドはPerson.メソッド名()で、インスタンスメソッドはインスタンス.メソッド名()で呼び出せます。

[リスト8]クラスメソッドとインスタンスメソッドを定義する
Person = sequelize.define 'Person',
  firstName:   Sequelize.STRING
  lastName:    Sequelize.STRING
,
  # クラスメソッドの定義
  classMethods:
    # lastNameが「宮沢」のレコードをすべて取得する
    getAllMiyazawa: (callback) ->
      @findAll({ where: { lastName: '宮沢' } }).success (persons) ->
        callback null, persons
      .error (error) ->
        callback error

  # インスタンスメソッドの定義
  instanceMethods:
    # フルネームを返す
    getFullName: ->
      "#{@lastName} #{@firstName}"

 リスト8で定義したPersonモデルの使用例はリスト9のようになります。非同期型関数であるPerson.getAllMiyazawa()の実行中にエラーが発生する可能性があるため、実際にエラーが発生したかどうかをコールバックの第1引数で伝えています。このように、エラーが発生する可能性のある非同期型関数では、エラーを知らせるためのスタイルを統一しておくとよいでしょう。

[リスト9]クラスメソッドとインスタンスメソッドの使用例
Person.getAllMiyazawa (err, persons) ->
  if err  # エラーが発生した
    console.log "db error: #{err}"
    return

  # 成功時
  for person in persons
    console.log person.getFullName()

SQLの実行

 SequelizeでSQLを直接実行するにはリスト10のようにquery()メソッドを呼びます。

[リスト10]SQLを直接実行する
# 単純なオブジェクトの配列として受け取る場合
sequelize.query('SELECT * FROM Persons', null, {raw: true}).success (rows) ->
  console.log rows.length
  console.log rows[0].firstName

# Personインスタンスの配列として受け取る場合
sequelize.query('SELECT * FROM Persons', Person).success (persons) ->
  console.log persons.length
  console.log persons[0].getFullName()

# 結果を受け取らない場合
sequelize.query("UPDATE Persons SET updatedAt=NOW()").success ->
  console.log "updated"

 O/Rマッパーは開発初期段階で大幅に工数を短縮できるため便利ですが、O/Rマッパーが背後で発行するSQLの種類には限りがあるため、自分で直接SQLを記述する場合と比べて多少余分な操作が入ってしまうことがあります。とくに大量の操作を一度に行うような場合、O/Rマッパーのモデルを使うよりもSQLを直接書いた方が遥かに高速に処理できます。一通りO/Rマッパーでの実装が終わりアプリケーションの高速化が必要な段階になったら、ボトルネックになっている部分をSQLで置き換えていくとよいでしょう。

トランザクション

 執筆時点での最新版であるSequelize 1.3.7ではトランザクション機能がサポートされていません。トランザクションを使うにはSQLを直接書く必要があります(リスト11)。

[リスト11]トランザクションを使う
doAtomicInsert = (cb) ->
  sequelize.query("BEGIN").error(cb).success ->
    sequelize.query("INSERT INTO Persons (firstName, lastName) " +
    "VALUES ('賢治', '宮沢')").error(cb).success ->
      sequelize.query("INSERT INTO Persons (firstName, lastName) " +
      "VALUES ('白秋', '北原')").error(cb).success ->
        sequelize.query("COMMIT").error(cb).success ->
          cb null

# 実行
doAtomicInsert (err) ->
  if err  # エラーが発生した
    console.log "db error: #{err.number}: #{err}"
  else
    console.log "commit ok"

 長いトランザクションが頻繁に実行されるとデッドロックが発生する可能性が高まります。トランザクションを頻繁にコミットするなどしてデッドロックを発生させないように、また万が一デッドロックが発生してもトランザクションを再実行するなどして適切に対処できるように、開発時に注意しましょう。

その他のAPI

 その他のAPIについてはドキュメント(英語)を参照してください。

次のページ
SQLiteに直接アクセスする

この記事は参考になりましたか?

CoffeeScriptによるモダンなWebアプリケーション開発連載記事一覧

もっと読む

この記事の著者

飯塚 直(イイヅカ ナオ)

1984年東京都生まれ。 高校時代に趣味でPerlやJavaを使ってプログラミングを始める。 慶応大学湘南藤沢キャンパス卒業後、共同通信社にてニュースサイトの開発などを担当。 その後、面白法人カヤックにてソーシャルゲームの開発などを手がける。 2012年現在、カヤックを退社し個人として活動し...

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

CodeZine(コードジン)
https://codezine.jp/article/detail/6529 2012/05/02 14:00

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー