🔖

Sequelize ORM から TiDB Serverless に接続する

に公開

過去2種類のORMを用いてTiDB Serverlessへの接続を行ってみました。

Drizzle
https://zenn.dev/kameoncloud/articles/c15ffb44d177b9

Prisma
https://zenn.dev/kameoncloud/articles/5de3ad5f68a220

今日は3つ目のSequelizeを使ってみます。

Sequelize とは

Node.js 向けの 老舗 ORM(Object-Relational Mapping)ライブラリ で過去取り上げたPrisma(2018年)、Drizzle(2022年)と比べその歴史は古く2011年リリースとのことです。
そのリリース時期から推察できるようにTypeScript対応の型推論に少し難があるようで、その後PrismaやDrizzleといった新しいORMが生まれたという背景があるようです。これはSequelizeはもともとJavaScriptをベースとして作成されており、後からTypeScript対応が行われたことが影響しているようです。TypeScriptが生まれたのはSequelizeより後の2012年ですので仕方のないこととも言えそうです。
とはいえ老舗である分ユーザーは非常に多く新規で採用されるケースは少ないかもしれませんが、やってみて纏めておく価値はありそうです。

やってみる

まずは必要ライブラリをインストールします。

npm install sequelize mysql2

sequelizeはORMとしての機能を提供していますがデータベースそのものへの接続機能を包含していません。このためMySQLへの接続にはmysql2を用いています。もちろん他のデータベースエンジンであればpg等別のライブラリが必要です。
次にテスト用JavaScriptファイルを作成します。

db.js
// db.js
const { Sequelize, DataTypes } = require('sequelize');

// TiDB Serverless 接続情報をここに設定
const sequelize = new Sequelize('test', '<userid>', '<password>', {
  host: 'gateway01.us-west-2.prod.aws.tidbcloud.com',
  port: 4000,
  dialect: 'mysql',
  dialectOptions: {
    ssl: {
      minVersion: 'TLSv1.2',
      rejectUnauthorized: true
    }
  },
  logging: console.log, // 必要に応じてログ出力
});

// テーブルの定義
const User = sequelize.define('User', {
  name: {
    type: DataTypes.STRING,
  },
  email: {
    type: DataTypes.STRING,
  }
});

// メイン処理
async function main() {
  try {
    await sequelize.authenticate();
    console.log('✅ Connection has been established successfully.');

    await sequelize.sync({ force: true }); // 初回のみ
    console.log('✅ All models were synchronized successfully.');

    const newUser = await User.create({ name: 'Alice', email: 'alice@example.com' });
    console.log('✅ Inserted user:', newUser.toJSON());

    const users = await User.findAll();
    console.log('✅ All users:', users.map(user => user.toJSON()));
  } catch (error) {
    console.error('❌ Unable to connect to the database:', error);
  } finally {
    await sequelize.close();
  }
}

main();

<userid>,<password>につていは皆さんの値に置き換えておいてください。
node db.jsを実行すると以下が表示されます。

Executing (default): SELECT 1+1 AS result
✅ Connection has been established successfully.
Executing (default): DROP TABLE IF EXISTS `Users`;
Executing (default): SELECT CONSTRAINT_NAME as constraint_name,CONSTRAINT_NAME as constraintName,CONSTRAINT_SCHEMA as constraintSchema,CONSTRAINT_SCHEMA as constraintCatalog,TABLE_NAME as tableName,TABLE_SCHEMA as tableSchema,TABLE_SCHEMA as tableCatalog,COLUMN_NAME as columnName,REFERENCED_TABLE_SCHEMA as referencedTableSchema,REFERENCED_TABLE_SCHEMA as referencedTableCatalog,REFERENCED_TABLE_NAME as referencedTableName,REFERENCED_COLUMN_NAME as referencedColumnName FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE where TABLE_NAME = 'Users' AND CONSTRAINT_NAME!='PRIMARY' AND CONSTRAINT_SCHEMA='test' AND REFERENCED_TABLE_NAME IS NOT NULL;  
Executing (default): DROP TABLE IF EXISTS `Users`;
Executing (default): DROP TABLE IF EXISTS `Users`;
Executing (default): CREATE TABLE IF NOT EXISTS `Users` (`id` INTEGER NOT NULL auto_increment , `name` VARCHAR(255), `email` VARCHAR(255), `createdAt` DATETIME NOT NULL, `updatedAt` DATETIME NOT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB;
Executing (default): SHOW INDEX FROM `Users`
✅ All models were synchronized successfully.
Executing (default): INSERT INTO `Users` (`id`,`name`,`email`,`createdAt`,`updatedAt`) VALUES (DEFAULT,?,?,?,?);
✅ Inserted user: {
  id: 1,
  name: 'Alice',
  email: 'alice@example.com',
  updatedAt: 2025-07-01T08:23:03.817Z,
  createdAt: 2025-07-01T08:23:03.817Z
}
Executing (default): SELECT `id`, `name`, `email`, `createdAt`, `updatedAt` FROM `Users` AS `User`;
✅ All users: [
  {
    id: 1,
    name: 'Alice',
    email: 'alice@example.com',
    createdAt: 2025-07-01T08:23:03.000Z,
    updatedAt: 2025-07-01T08:23:03.000Z
  }
]

以下3つのSQLが実行されています。

CREATE TABLE IF NOT EXISTS `Users` (`id` INTEGER NOT NULL auto_increment , `name` VARCHAR(255), `email` VARCHAR(255), `createdAt` DATETIME NOT NULL, `updatedAt` DATETIME NOT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB;

INSERT INTO `Users` (`id`,`name`,`email`,`createdAt`,`updatedAt`) VALUES (DEFAULT,?,?,?,?);

SELECT `id`, `name`, `email`, `createdAt`, `updatedAt` FROM `Users` AS `User`;

validation

ではsequelizevalidation機能を使ってみます。
これはSQLで取り扱う文字列に対してあらかじめ入力規則などを指定しておくことで、意図しない値がテーブルに挿入されることを防ぐ、などを行えます。db.jsを以下に修正します。

db.js
// db.js
const { Sequelize, DataTypes, ValidationError } = require('sequelize');

// TiDB Serverless 接続情報をここに記述
const sequelize = new Sequelize('test', '<userid>', '<password>', {
  host: 'gateway01.us-west-2.prod.aws.tidbcloud.com',
  port: 4000,
  dialect: 'mysql',
  dialectOptions: {
    ssl: {
      minVersion: 'TLSv1.2',
      rejectUnauthorized: true
    }
  },
  logging: false
});

// バリデーション付きモデル定義
const User = sequelize.define('User', {
  name: {
    type: DataTypes.STRING,
    allowNull: false,
    validate: {
      len: {
        args: [3, 50],
        msg: '名前は3文字以上50文字以内で入力してください。'
      }
    }
  },
  email: {
    type: DataTypes.STRING,
    allowNull: false,
    unique: true,
    validate: {
      isEmail: {
        msg: '有効なメールアドレスを入力してください。'
      }
    }
  }
}, {
  tableName: 'users',
  timestamps: true
});

async function main() {
  try {
    await sequelize.authenticate();
    console.log('✅ 接続成功');

    await sequelize.sync({ force: true }); // テーブル作成
    console.log('✅ テーブル作成完了');

    // 正常なデータ(成功する)
    try {
      const validUser = await User.create({ name: 'Alice', email: 'alice@example.com' });
      console.log('✅ バリデーション成功:', validUser.toJSON());
    } catch (err) {
      console.error('❌ バリデーションエラー(正):', err.errors.map(e => e.message));
    }

    // nameが短い(失敗する)
    try {
      await User.create({ name: 'Bo', email: 'bob@example.com' });
    } catch (err) {
      console.error('❌ バリデーションエラー(name):', err.errors.map(e => e.message));
    }

    // emailが不正(失敗する)
    try {
      await User.create({ name: 'Charlie', email: 'not-an-email' });
    } catch (err) {
      console.error('❌ バリデーションエラー(email):', err.errors.map(e => e.message));
    }

  } catch (err) {
    console.error('❌ 接続エラー:', err);
  } finally {
    await sequelize.close();
    console.log('✅ 接続終了');
  }
}

main();

例えば以下の書式でnameカラムは3-50文字として定義しています。

name: {
    type: DataTypes.STRING,
    allowNull: false,
    validate: {
      len: {
        args: [3, 50],
        msg: '名前は3文字以上50文字以内で入力してください。'
      }
    }
  },

メールアドレスの入力チェックはよりシンプルにisEmailで実行が可能です。

 email: {
    type: DataTypes.STRING,
    allowNull: false,
    unique: true,
    validate: {
      isEmail: {
        msg: '有効なメールアドレスを入力してください。'
      }
    }
  }
},

その他isUrl, isNumeric, notEmptyなどが定義されています。

Association

もう一つ機能を試してみます。AssociationとはORM上でテーブル間のリレーション、RDBでいうところのForeign Keyを設定する機能です。これによりSQLを書かなくてもリレーションが張られている複数テーブルのデータをまとめて取り出すことができるようになります。(もちろんその分やり取りされうデータ量は増えていきますので注意は必要です)
再度db.jsを更新します。

db.js
// db.js
const { Sequelize, DataTypes } = require('sequelize');

// TiDB Serverless 接続情報を設定
const sequelize = new Sequelize('test', '<userid>', '<password>', {
  host: 'gateway01.us-west-2.prod.aws.tidbcloud.com',
  port: 4000,
  dialect: 'mysql',
  dialectOptions: {
    ssl: {
      minVersion: 'TLSv1.2',
      rejectUnauthorized: true
    }
  },
  logging: false
});

// モデル定義:User
const User = sequelize.define('User', {
  name: {
    type: DataTypes.STRING,
    allowNull: false
  },
  email: {
    type: DataTypes.STRING,
    allowNull: false
  }
}, {
  tableName: 'users',
  timestamps: true
});

// モデル定義:Post
const Post = sequelize.define('Post', {
  title: {
    type: DataTypes.STRING,
    allowNull: false
  },
  content: {
    type: DataTypes.TEXT
  }
}, {
  tableName: 'posts',
  timestamps: true
});

// 関連設定(1:N)
User.hasMany(Post);       // User → Post(1:N)
Post.belongsTo(User);     // Post → User(N:1)

// メイン処理
async function main() {
  try {
    await sequelize.authenticate();
    console.log('✅ 接続成功');

    await sequelize.sync({ force: true });
    console.log('✅ テーブル作成完了');

    // ユーザーと投稿の作成(関連付けあり)
    const user = await User.create({
      name: 'Alice',
      email: 'alice@example.com',
      Posts: [
        { title: 'はじめての投稿', content: 'Sequelize楽しい!' },
        { title: '続報', content: 'TiDB Serverlessとも連携できた!' }
      ]
    }, {
      include: Post // ← 関連を一緒にINSERT
    });

    console.log('✅ ユーザーと投稿を作成:', user.toJSON());

    // 関連付きで取得(User → Posts)
    const usersWithPosts = await User.findAll({
      include: {
        model: Post
      }
    });

    console.log('✅ 関連付きの取得結果:');
    usersWithPosts.forEach(user => {
      console.log(`- ${user.name} (${user.email})`);
      user.Posts.forEach(post => {
        console.log(`${post.title}: ${post.content}`);
      });
    });

  } catch (err) {
    console.error('❌ エラー:', err);
  } finally {
    await sequelize.close();
    console.log('✅ 接続終了');
  }
}

main();

ポイントとなるのは以下2行です。

User.hasMany(Post);       // User → Post(1:N)
Post.belongsTo(User);     // Post → User(N:1)

これはSQLでいう以下に相当します。

ALTER TABLE `posts` ADD COLUMN `UserId` INTEGER;
ALTER TABLE `posts` ADD CONSTRAINT `posts_UserId_fkey` FOREIGN KEY (`UserId`) REFERENCES `users` (`id`);

結果として

const usersWithPosts = await User.findAll({
  include: {
    model: Post
  }
});

この部分はFKを通じてテーブルをjoinしてSQLを実行しており以下に相当します。

SELECT
  `users`.`id` AS `User.id`,
  `users`.`name` AS `User.name`,
  `users`.`email` AS `User.email`,
  `users`.`createdAt` AS `User.createdAt`,
  `users`.`updatedAt` AS `User.updatedAt`,
  `posts`.`id` AS `Posts.id`,
  `posts`.`title` AS `Posts.title`,
  `posts`.`content` AS `Posts.content`,
  `posts`.`UserId` AS `Posts.UserId`,
  `posts`.`createdAt` AS `Posts.createdAt`,
  `posts`.`updatedAt` AS `Posts.updatedAt`
FROM `users`
LEFT OUTER JOIN `posts` ON `users`.`id` = `posts`.`UserId`;

Discussion