背景
前回、sequelizeを利用したコードのtypescript化を試みて、modelの呼び出し元はtypescriptっぽく書くようにできたけど、モデル自体はtypescript化できずに、中途半端な感じで終わってしまった。
https://qiita.com/yusuke-ka/items/244d3dfafb578fd84b1a
sequelizeは、元々typescriptをサポートしていなかったため、typescriptと少し相性が悪いとの記事もいくつか見つけた。
そこで今回は最近伸びてきているTypeORMを試してみようと思う。
こちらは元々typescriptが前提となっているORMなので、typescript化で苦労することはなさそう。
googleトレンドで見ると、sequelizeに追いつく勢いで伸びてきている。
(むしろtypescriptに限定すれば、既にTypeORMの方が人気がある気がする)
TypeORMのCLIを使った環境構築
DBは以前インストールしたpostgresql(windows)を利用する。
https://qiita.com/yusuke-ka/items/448843020c0406363ba5#%E6%BA%96%E5%82%99
pgadmin4でデータベースインスタンスだけ作っておく。
データベース名("typeorm"とした)を入力して作成。
ここからは、コード エディタ(VS Code)上での作業。
まずはベースとなるnode環境を作る。
> mkdir typeorm
> cd typeorm
> yarn init
検証なので、とりあえず全部デフォルト設定。
続いて、TypeORMのインストール。
また、DBはpostgresqlを使うのでpg(node-postgres)もインストール。
> yarn add typeorm
> yarn add pg
TypeORMのCLIがyarnで簡単に使えるようにpackage.jsonに以下を追加しておく。
{..."scripts":{"typeorm-cli":"typeorm"},...}
TypeORMのCLIを使って、express、postgresqlのTypeORM環境を一気に構築。
> yarn typeorm-cli init --express --database postgres
自動で以下のようなフォルダ/ファイルが生成される。
|- typeorm/
|- src/
|- controller/
|- UserController.ts
|- entry/
|- User.ts
|- migration/
|- index.ts
|- routes.ts
|- ormconfig.json
|- tsconfig.json
設定ファイル(ormconfig.json)で接続先DBに合わせて設定を変更。
{"type":"postgres","host":"localhost","port":5432,"username":"postgres","password":"postgres","database":"typeorm","synchronize":true,"logging":false,"entities":["src/entity/**/*.ts"],"migrations":["src/migration/**/*.ts"],"subscribers":["src/subscriber/**/*.ts"],"cli":{"entitiesDir":"src/entity","migrationsDir":"src/migration","subscribersDir":"src/subscriber"}}
今回は以下だけ変更。
..."username":"postgres","password":"postgres","database":"typeorm",...
これでTypeORMの環境構築は完了。簡単ですね。
TypeORMのCLIで自動構築された環境の確認
CLIで作成すると、サンプルとして"User"というモデルを操作するコードが自動で入っている。
自動生成されたモデル(src/entry/User.ts)は以下のようになっている。
import{Entity,PrimaryGeneratedColumn,Column}from"typeorm";@Entity()exportclassUser{@PrimaryGeneratedColumn()id:number;@Column()firstName:string;@Column()lastName:string;@Column()age:number;}
モデルを定義するときは、これを真似して<モデル名>.tsをentryフォルダ以下に配置すればよい模様。
続いて、コントローラー(src/controller/UserController.ts)は以下のようになっていた。
import{getRepository}from"typeorm";import{NextFunction,Request,Response}from"express";import{User}from"../entity/User";exportclassUserController{privateuserRepository=getRepository(User);asyncall(request:Request,response:Response,next:NextFunction){returnthis.userRepository.find();}asyncone(request:Request,response:Response,next:NextFunction){returnthis.userRepository.findOne(request.params.id);}asyncsave(request:Request,response:Response,next:NextFunction){returnthis.userRepository.save(request.body);}asyncremove(request:Request,response:Response,next:NextFunction){letuserToRemove=awaitthis.userRepository.findOne(request.params.id);awaitthis.userRepository.remove(userToRemove);}}
ここではモデルに対する操作を書く感じですね。
全件取得、1件取得、保存(作成/更新)、削除の機能がサンプルとして実装されている。
getRepository(Hoge)で取得されるRepositoryをTypeORMが提供してくれているので、このRepositoryのメソッド(findやsaveなど)を呼び出して、DBにアクセスしているっぽい。
次は、index.ts。expressを使用する感じで自動生成されている。
import"reflect-metadata";import{createConnection}from"typeorm";import*asexpressfrom"express";import*asbodyParserfrom"body-parser";import{Request,Response}from"express";import{Routes}from"./routes";import{User}from"./entity/User";createConnection().then(asyncconnection=>{// create express appconstapp=express();app.use(bodyParser.json());// register express routes from defined application routesRoutes.forEach(route=>{(appasany)[route.method](route.route,(req:Request,res:Response,next:Function)=>{constresult=(new(route.controllerasany))[route.action](req,res,next);if(resultinstanceofPromise){result.then(result=>result!==null&&result!==undefined?res.send(result):undefined);}elseif(result!==null&&result!==undefined){res.json(result);}});});// setup express app here// ...// start express serverapp.listen(3000);// insert new users for testawaitconnection.manager.save(connection.manager.create(User,{firstName:"Timber",lastName:"Saw",age:27}));awaitconnection.manager.save(connection.manager.create(User,{firstName:"Phantom",lastName:"Assassin",age:24}));console.log("Express server has started on port 3000. Open http://localhost:3000/users to see results");}).catch(error=>console.log(error));
routes.tsに定義されているRouteをforEachで回しているようなので、新しいAPIを追加するときには、このコード(index.ts)は変更せずに、routes.tsに定義を追加すれば良さそう。
最後のほうにある
// insert new users for test
以下はサーバー起動時にテストデータを入れているコードのようなので、実際に利用する際には消しておいた方がよさそう。
というか必要ないので、さっそく消しておく。
import"reflect-metadata";import{createConnection}from"typeorm";import*asexpressfrom"express";import*asbodyParserfrom"body-parser";import{Request,Response}from"express";import{Routes}from"./routes";createConnection().then(asyncconnection=>{// create express appconstapp=express();app.use(bodyParser.json());// register express routes from defined application routesRoutes.forEach(route=>{(appasany)[route.method](route.route,(req:Request,res:Response,next:Function)=>{constresult=(new(route.controllerasany))[route.action](req,res,next);if(resultinstanceofPromise){result.then(result=>result!==null&&result!==undefined?res.send(result):undefined);}elseif(result!==null&&result!==undefined){res.json(result);}});});// setup express app here// ...// start express serverapp.listen(3000);console.log("Express server has started on port 3000. ");}).catch(error=>console.log(error));
routes.tsを見てみる。これも自動生成されている。
import{UserController}from"./controller/UserController";exportconstRoutes=[{method:"get",route:"/users",controller:UserController,action:"all"},{method:"get",route:"/users/:id",controller:UserController,action:"one"},{method:"post",route:"/users",controller:UserController,action:"save"},{method:"delete",route:"/users/:id",controller:UserController,action:"remove"}];
サンプルとして、全件取得、1件取得、追加(更新)、削除の4つのAPIが定義されている模様。
最後に設定系のファイルを見てみる。
typeORMの設定ファイル(ormconfig.json)は、先ほど見たので、package.jsonから。
{"name":"typeorm","version":"1.0.0","main":"index.js","license":"MIT","scripts":{"typeorm-cli":"typeorm","start":"ts-node src/index.ts"},"dependencies":{"typeorm":"0.2.25","reflect-metadata":"^0.1.10","pg":"^7.3.0","express":"^4.15.4","body-parser":"^1.18.1"},"devDependencies":{"ts-node":"3.3.0","@types/node":"^8.0.29","typescript":"3.3.3333"}}
scriptsにstartが追加されている。これでサーバーを実行する模様。
あとは、必要な依存が自動的に追加されている。
tsconfig.jsonも確認。
{"compilerOptions":{"lib":["es5","es6"],"target":"es5","module":"commonjs","moduleResolution":"node","outDir":"./build","emitDecoratorMetadata":true,"experimentalDecorators":true,"sourceMap":true}}
typeORMのREADMEに書いてある設定が自動的に入っているようです。
https://github.com/typeorm/typeorm#installation
TypeORMのサンプルコードを実行してみる。
TypeORMのコードを実行してみようと思う。
まずは、モデルをDBに反映してみる。
migration:generateを実行すると、DBの内容とモデルを比較してマイグレーションファイルを作成してくれるようなので、これを実行してみる。
> yarn typeorm-cli migration:generate -n UserMigration
エラーが発生した。
yarn run v1.22.4
$ typeorm migration:generate -n UserMigration
Error during migration generation:
D:\study\orm\typeorm\src\entity\User.ts:1
import {Entity, PrimaryGeneratedColumn, Column} from "typeorm";
^^^^^^
SyntaxError: Cannot use import statement outside a module
...
本家サイトのここを参考にしてやってみる。
https://github.com/typeorm/typeorm/blob/master/docs/using-cli.md#installing-cli
package.jsonのscriptsを以下のように変更(typeormのscriptを追加)。
(ts-nodeは自動でインストールされているはずだけど、もし入っていなければ、インストールする必要があるかも)
...
"scripts": {
"typeorm-cli": "typeorm",
"typeorm": "node --require ts-node/register ./node_modules/typeorm/cli.js",
"start": "ts-node src/index.ts"
},
...
再度実行してみる。
(今度は、yarn typeorm-cliではなく、yarn typeorm)
> yarn typeorm migration:generate -n UserMigration
今度は成功。
yarn run v1.22.4
$ node --require ts-node/register ./node_modules/typeorm/cli.js migration:generate -n UserMigration
Migration D:\study\orm\typeorm/src/migration/1593226939171-UserMigration.ts has been generated successfully.
Done in 2.03s.
こんな感じでマイグレーションファイルが生成された。
import{MigrationInterface,QueryRunner}from"typeorm";exportclassUserMigration1593226939171implementsMigrationInterface{name='UserMigration1593226939171'publicasyncup(queryRunner:QueryRunner):Promise<void>{awaitqueryRunner.query(`CREATE TABLE "user" ("id" SERIAL NOT NULL, "firstName" character varying NOT NULL, "lastName" character varying NOT NULL, "age" integer NOT NULL, CONSTRAINT "PK_cace4a159ff9f2512dd42373760" PRIMARY KEY ("id"))`);}publicasyncdown(queryRunner:QueryRunner):Promise<void>{awaitqueryRunner.query(`DROP TABLE "user"`);}}
DBに反映させてみる。
> yarn typeorm migration:run
userテーブルが作成された。
サーバーを起動。
> yarn start
今回もchromeの拡張ツール「Advanced REST client」で動作確認。
http://localhost:3000/usersに各種リクエストを送ってみる。
routes.tsに定義されているRouteを見てURLを指定。
まずは全件取得。
何も登録してないので、結果は空の配列。
つづいて、ユーザーを登録してみる。
application/jsonで指定したパラメータでユーザーが登録された。
DBにも登録されている。
同様にもう一人ユーザーを追加した後、再度全件取得してみる。
今度は配列が空じゃない状態で返ってきた。
パスにIDを指定して取得。
1件だけ返ってきた。
データ更新。
パラメータにID(integer)を指定してPOSTリクエストを送る。
データが更新された。
最後に、削除を試してみる。
DBからデータが消えているようだが、レスポンスが返ってこない。
index.tsでresultがundefinedの時にレスポンスを返していないのが原因かと思われる。
...// register express routes from defined application routesRoutes.forEach(route=>{(appasany)[route.method](route.route,(req:Request,res:Response,next:Function)=>{constresult=(new(route.controllerasany))[route.action](req,res,next);if(resultinstanceofPromise){result.then(result=>result!==null&&result!==undefined?res.send(result):undefined);// ← ココ}elseif(result!==null&&result!==undefined){res.json(result);}});});...
試しに200を返すように書き換えてみる。
...// register express routes from defined application routesRoutes.forEach(route=>{(appasany)[route.method](route.route,(req:Request,res:Response,next:Function)=>{constresult=(new(route.controllerasany))[route.action](req,res,next);if(resultinstanceofPromise){result.then(result=>result!==null&&result!==undefined?res.send(result):res.send(200));// ← ココ}elseif(result!==null&&result!==undefined){res.json(result);}});});...
他のIDを指定して削除を再実行。
今度はレスポンスが返ってきた。
修正が正しいかどうかは置いといて、原因は特定できた。
さいごに
今回はTypeORMの環境構築を試してみた。
いくつかハマりポイントはあったもののsequelizeをtypescript化するよりは遥かに楽だったし、モデルもコントローラーもすべてtypescriptで書けそうなので、sequelizeの時のように中途半端な感じにならないのも良さげ。
(もう少し時間をかけて調べれば、sequelizeもいい感じにtypescript化できる方法があるのかもしれないけど…)
sequelizeほど成熟していない感はあるものの、typescriptで書くことが前提ならTypeORMの方がやりやすい気がします。