ogochan
今、新しい商品紹介サイトを作っています。
詳しくはリリースの時に書きますが、ここでレビューしたものや良いと思ったものの情報を集めて、「使い方」をちゃんと書いて行こうと考えています。
その中でOrange Pi 5についての記事があるのですが、ここに既に書いたここのブログの記事を入れたいなと思っています。
このためにAPIサーバを作ったという話です。
背景
大昔に書いたように、弊社ホームページはRailsで自作した記事管理アプリと、Sinatraで自作した表示系とで構成されています。つまり、ActiveRecordを使っています。
ところが最近の弊社はRailsなものと言うかRubyなものから離れていて、ウェブアプリは主にNode.jsで作っています。
既にRubyスタック上で作られた資産がある上にそのAPIを作るだけなのでRubyで書いてしまえば良いとも思ったのですが、弊社で今からRubyスタックなものを作ると技術的負債になりかねないので、Node.jsで作りたいと思いました。
とは言え、既に安定して動いているCMS環境を全部書き直す気もなければ必要性もないし、そもそもそんな時間も手間もありません。
そこで、
APIの部分だけNode.jsで作る
ということにしました。
実装
わざわざ見出しをつける程のことではないのですが、Node.js + Express + Sequelizeで実装をします。
今回はCMSのヘッドレス化だけなので、APIが実装できれば良いため、フロントに相当する部分は作りません。
データベースは出来ればCMS環境のものをそのまま使いたいので、SequelizeでなくてもNode.jsでRailsで作ったデータベースを操作する必要があります。
考察
ActiveRecordもSequelizeもORマッパーです。そして、どちらもデータ永続化に使うテーブルの先頭には、idという名前でoidのフィールドが存在しています。
また、RubyにしろJavascriptにしろ、表面的には型のない言語であり、データベースの型とそれぞれの処理系の型は、処理系の中で「よしな」にしてくれています。
となると、考えるべきは、
- oid生成の互換性
- データベースの型と言語処理系のデータの扱いの互換性
- その他のルールの整合性
についてです
なお、以下の話は全てPostgreSQLを使っていることが前提です。他のDBの時はまた違うかも知れないので注意が必要です。
oid生成の互換性
まずはoidの生成の互換性について調べます。
まず、このCMSのテーブル一覧
cms_production=# \d
List of relations
Schema | Name | Type | Owner
--------+--------------------------+----------+---------
public | ar_internal_metadata | table | ogochan
public | categories | table | ogochan
public | categories_id_seq | sequence | ogochan
public | entries | table | ogochan
public | entries_id_seq | sequence | ogochan
public | media_cache_files | table | ogochan
public | media_cache_files_id_seq | sequence | ogochan
public | media_files | table | ogochan
public | media_files_id_seq | sequence | ogochan
public | schema_migrations | table | ogochan
public | users | table | ogochan
public | users_id_seq | sequence | ogochan
(12 rows)
こんな感じです。
これを見ると、データの入っているテーブル(Typeがtableなもの)と連番を管理しているもの(Typeがsequenceなもの)とがあることがわかります。
たとえば、このうちのusersとusers_id_seqについて見ると、
cms_production=# \d users
Table "public.users"
Column | Type | Collation | Nullable | Default
------------------+-----------------------------+-----------+----------+-----------------------------------
id | integer | | not null | nextval('users_id_seq'::regclass)
name | character varying | | |
login_name | character varying | | |
hashed_password | character varying | | |
salt | character varying | | |
lock_time | timestamp without time zone | | |
mail_address | character varying | | |
created_at | timestamp without time zone | | |
updated_at | timestamp without time zone | | |
profile_image_id | integer | | |
Indexes:
"users_pkey" PRIMARY KEY, btree (id)
cms_production=# \d users_id_seq
Sequence "public.users_id_seq"
Type | Start | Minimum | Maximum | Increment | Cycles? | Cache
--------+-------+---------+---------------------+-----------+---------+-------
bigint | 1 | 1 | 9223372036854775807 | 1 | no | 1
Owned by: public.users.id
cms_production=#
となっています。
Sequelizeで作ったデータベースの方で似たようなテーブルを見てみます。
kaikei_production=# \d "Users"
Table "public.Users"
Column | Type | Collation | Nullable | Default
---------------+--------------------------+-----------+----------+-------------------------------------
id | integer | | not null | nextval('"Users_id_seq"'::regclass)
name | character varying(255) | | |
hash_password | character varying(255) | | |
createdAt | timestamp with time zone | | not null |
updatedAt | timestamp with time zone | | not null |
Indexes:
"Users_pkey" PRIMARY KEY, btree (id)
kaikei_production=# \d "Users_id_seq"
Sequence "public.Users_id_seq"
Type | Start | Minimum | Maximum | Increment | Cycles? | Cache
---------+-------+---------+------------+-----------+---------+-------
integer | 1 | 1 | 2147483647 | 1 | no | 1
Owned by: public."Users".id
kaikei_production=#
関係ないフィールドがある部分を無視すれば、大変よく似ている... と言うより、だいたい同じであることがわかります。特にいずれも連番管理の情報があることや、idフィールドのデフォルト値とか同じですね。つまり、
AcriveRecordもSequelizeもoidの生成論理は同じだろう
ということがわかります。つまり、互換性があるものと期待しても良いと推測します。本当はコードを見ればいいんでしょうが。
データベースの型と言語処理系のデータの扱いの互換性
これも前の例を見てもらうと見当がつきます。
ActiveRecordのintegerとSequelizeのINTEGERは、いずれもSQL上ではintegerです。
AcriveRecordのstringとSequelizeのSTRINGは、前者がcharacter varying
であり、後者がcharacter varying(255)
です。違うと言えば違いますし、だいたい同じとも言えます。
ということで、どちらも変な小細工はしてない感じなので、「同じだろう」とヤマを張ります。本当はコードを見ればいいんでしょうが。
その他のルールの整合性
どちらにもエントリの作成された時刻と更新された時刻が保存されているフィールドがあります。どちらもtimestamp without time zone
です。このフィールドの更新は、処理系の中でやっているようです。
ただ、見掛け上大きな違いは、命名規約が異なるということです。
他のフィールドであれば、テーブルでの命名に従って気にしないという手もありますが、このような処理系に依存する部分はどうなりますかね(伏線)
実装
考察で「まぁだいたいけるんじゃね?」という見当をつけたところで、Sequelizeの./models
を書いてみます。
まずはテーブル上の定義
cms_production=# \d entries
Table "public.entries"
Column | Type | Collation | Nullable | Default
--------------+-----------------------------+-----------+----------+-------------------------------------
id | integer | | not null | nextval('entries_id_seq'::regclass)
author_id | integer | | |
name | character varying | | |
page_name | character varying | | |
title | character varying | | |
body | text | | |
published_at | date | | |
released_at | date | | |
status | integer | | |
created_at | timestamp without time zone | | |
updated_at | timestamp without time zone | | |
top | integer | | |
category_id | integer | | |
modified_at | date | | |
subtitle | character varying | | |
Indexes:
"entries_pkey" PRIMARY KEY, btree (id)
cms_production=#
これは記事のテーブルですね。
ちなみに、ActiveRecordの中のdb/schema.rbは
create_table "entries", id: :serial, force: :cascade do |t|
t.integer "author_id"
t.string "name"
t.string "page_name"
t.string "title"
t.text "body"
t.date "published_at"
t.date "released_at"
t.integer "status"
t.datetime "created_at"
t.datetime "updated_at"
t.integer "top"
t.integer "category_id"
t.integer "thumbnail"
t.date "modified_at"
t.string "subtitle"
end
となっています。
こいつをSequelizeのmodelsで表現すると、
'use strict';
const {
Model
} = require('sequelize');
module.exports = (sequelize, DataTypes) => {
class Entry extends Model {
(略)
}
Entry.init({
authorId: {
type: DataTypes.INTEGER,
field: 'author_id' // <--- (4)
},
name: DataTypes.STRING,
page_name: DataTypes.STRING,
title: DataTypes.STRING,
body: DataTypes.TEXT,
publishedAt: {
type: DataTypes.DATE,
field: 'published_at'
},
releasedAt: {
type: DataTypes.DATE,
field: 'released_at'
},
status: DataTypes.INTEGER,
top: DataTypes.INTEGER,
categoryId: {
type: DataTypes.INTEGER,
field: 'category_id'
},
modifiedAt: {
type: DataTypes.DATE,
field: 'modified_at'
},
subtitle: DataTypes.STRING,
}, {
sequelize,
modelName: 'Entry',
tableName: 'entries', // <--- (1)
createdAt: 'created_at', // <--- (2)
updatedAt: 'updated_at', // <--- (3)
});
return Entry;
};
という感じで。
まず、最初にすることは、(1)です。これはデータベース上のテーブル名をSequelizeに教えてやる部分です。
デフォルトではモデル名と同じであることを仮定するわけですが、ここでは元々あるテーブルに合わせてやる必要があること、命名規約がAcriveRecordとSequelizeとが異なるために、明示的に指定します。
(2)と(3)は自動的に更新されるフィールド名を合わせてやるためにフィールド名を明示しています。
これは、参照しかしない場合であっても必要です。SequelizeがSELECT文を自動生成する時に、自分の命名規約に従ったフィールド名を指定してしまうので、ここを指定しないと、「そんなフィールドはない」という実行時エラーが参照の時であっても出てしまいます。
(4)はオマケです。これは指定してもしなくても構いません。ActiveRecord的な命名でも気にならないのであれば、<フィールド名> : <型> という形式で問題はありませんが、Sequelize流と言うかJavascriptっぽい命名にしたければ、このように指定する必要があります。どっちが優れているというものではないわけですが、コード中に下手に混ざってしまうとみっともない上に読み辛いので、私はこのようにしています。
結果
こんな感じでそれぞれのテーブルに対応するようなmodelsを書いて、参照や更新をしてみました。
今のところ特に問題となるような動作はしていません。つまり、ActiveRecordとSequelizeが同じデータベースを使うことは成功しているようです。
留意点
割と簡単にうまく行ったのですが、注意が必要な部分もあります。それは、(データベースの)フィールド名と属性名(クラスの中に書いてあるやつ)がイコールでないことが出て来るため、「この文脈ではどっちであるか」ということを意識しなければならないということです。
具体的には、SQLそのものになる部分、たとえばfindで使うwhere、whereの中は属性名で指定しなければなりませんが、orderの中ではフィールド名です。
また、associateの中で指定する、foreignKeyは属性名を指定します。
これらのことは、フィールド名と属性名を異なるものにした時に起きることであって、ActiveRecordがどうこうとは関係ないことですが、注意が必要です。