プロローグ
今回はSequelizeでエンティティ間のリレーションを定義する方法と、定義したリレーションに基づいて結合演算をする方法を紹介していくンゴ。結合演算はこのあと出てくるER図のエンティティ、リレーションを対象に行う。また、リクエストを送信すると結合演算の結果をjson形式で返すAPIサーバを構築し、レスポンスを確認する形で結合演算の結果を確認していく。
開発環境
sequelize: 6.21.0
sequelize-cli: 6.4.1
node: 17.1.0
express: 4.18.1
今回定義するエンティティとリレーション
-
Student-StudentClass
: 1対多(※StudentClass
はClass
とStudent
の多対多の関係性を定義するための連関テーブル) -
Class-StudentClass
: 1対多 -
Student-Class
: 多対多 -
Student-Faculty
: 多対1 -
Class-Teacher
: 多対1(※Teacher
は教授テーブル。学部長である場合はFaculty
テーブルのレコードから外部参照される) -
Faculty-Teacher
: 1対1(※学部と学部長のリレーション)
モデルファイルでリレーションを定義
モデルはSequelize-cli
で作成したモデルファイルを利用しているので、それらに対してリレーションに関する記述を追加していく。
具体的には、各モデルをcliで作成した際に作成されるモデルファイルの// define association here
とあるコメント部分に追加することになる。
class Hoge extends Model {
/**
* Helper method for defining associations.
* This method is not a part of Sequelize lifecycle.
* The `models/index` file will call this method automatically.
*/
static associate(models) {
// define association here
}
}
以下はリレーションに関する記述を追加した後のモデルファイル。
'use strict';
const {
Model
} = require('sequelize');
module.exports = (sequelize, DataTypes) => {
class Class extends Model {
/**
* Helper method for defining associations.
* This method is not a part of Sequelize lifecycle.
* The `models/index` file will call this method automatically.
*/
static associate(models) {
Class.belongsToMany(models.Student ,{through: 'StudentClass', foreignKey: 'classCode'});
Class.belongsTo(models.Teacher, {foreignKey: 'teacherNo'});
}
}
Class.init({
classCode: {
type: DataTypes.STRING,
primaryKey: true
},
className: DataTypes.STRING
}, {
sequelize,
modelName: 'Class',
freezeTableName: true,
paranoid: true,
quoteIdentifiers:false
});
return Class;
};
'use strict';
const {
Model
} = require('sequelize');
module.exports = (sequelize, DataTypes) => {
class Faculty extends Model {
/**
* Helper method for defining associations.
* This method is not a part of Sequelize lifecycle.
* The `models/index` file will call this method automatically.
*/
static associate(models) {
Faculty.hasMany(models.Student, {foreignKey: 'facultyNo'});
Faculty.belongsTo(models.Teacher, {foreignKey: 'deanNo'});
}
}
Faculty.init({
facultyNo: {
type: DataTypes.STRING,
primaryKey: true
},
deanNo: {
type: DataTypes.STRING,
allowNull: false
},
facultyName: DataTypes.STRING
}, {
sequelize,
modelName: 'Faculty',
freezeTableName: true,
paranoid: true,
quoteIdentifiers: false
});
return Faculty;
};
'use strict';
const {
Model
} = require('sequelize');
module.exports = (sequelize, DataTypes) => {
class Student extends Model {
/**
* Helper method for defining associations.
* This method is not a part of Sequelize lifecycle.
* The `models/index` file will call this method automatically.
*/
static associate(models) {
Student.belongsToMany(models.Class, {through: 'StudentClass', foreignKey: 'studentNo'});
Student.belongsTo(models.Faculty, {foreignKey: 'facultyNo'});
}
}
Student.init({
studentNo: {
type: DataTypes.STRING,
primaryKey: true
},
firstName: {
type: DataTypes.STRING,
allowNull: false
},
lastName: {
type: DataTypes.STRING,
allowNull: false
}
}, {
sequelize,
modelName: 'Student',
freezeTableName: true,
paranoid: true,
quoteIdentifiers:false
});
return Student;
};
'use strict';
const {
Model
} = require('sequelize');
module.exports = (sequelize, DataTypes) => {
class StudentClass extends Model {
/**
* Helper method for defining associations.
* This method is not a part of Sequelize lifecycle.
* The `models/index` file will call this method automatically.
*/
static associate(models) {
/*StudentClass.belongsToMany(models.Class, {through: 'StudentClass'});
StudentClass.belongsTo(models.Faculty, { foreignKey: 'facultyNo'});*/
}
}
StudentClass.init({
studentNo: {
type: DataTypes.STRING,
primaryKey: true,
references: {
model: 'Student',
key: 'studentNo'
}
},
classCode: {
type: DataTypes.STRING,
primaryKey: true,
references: {
model: 'Class',
key: 'classCode'
}
}
}, {
sequelize,
modelName: 'StudentClass',
freezeTableName: true,
paranoid: true,
quoteIdentifiers:false
});
return StudentClass;
};
'use strict';
const {
Model
} = require('sequelize');
module.exports = (sequelize, DataTypes) => {
class Teacher extends Model {
/**
* Helper method for defining associations.
* This method is not a part of Sequelize lifecycle.
* The `models/index` file will call this method automatically.
*/
static associate(models) {
Teacher.hasMany(models.Class, {foreignKey: 'teacherNo'});
Teacher.hasOne(models.Faculty, {foreignKey: 'deanNo'});
}
}
Teacher.init({
teacherNo: {
type: DataTypes.STRING,
primaryKey: true
},
firstName: {
type: DataTypes.STRING,
allowNull: false
},
lastName: {
type: DataTypes.STRING,
allowNull: false
}
}, {
sequelize,
modelName: 'Teacher',
freezeTableName: true,
paranoid: true,
quoteIdentifiers:false
});
return Teacher;
};
以上のように、リレーションは各モデルファイルの
static associate(models) {}
の部分に記載していて、他のモデルを参照する際は引数のmodels
を使ってmodels.Hoge
と記述する。具体的なリレーションの定義の仕方は以下の通り
- 1対1
Teacher.hasOne(models.Faculty, {foreignKey: 'deanNo'});
Faculty.belongsTo(models.Teacher, {foreignKey: 'deanNo'});
{foreignKey: 'deanNo'}
で実際の外部キーを指定しているのがポイント。これは1対多
や多対多
のリレーションでも同じ。これがないとSequelizeが勝手に外部キーを予測してしまって、結果的に存在しないカラムを参照して、実行時にエラーになってしまう。
- 1対多
//1の側
Student.belongsTo(models.Faculty, {foreignKey: 'facultyNo'});
//多の側
Faculty.hasMany(models.Student, {foreignKey: 'facultyNo'});
- 多対多
Student.belongsToMany(models.Class, {through: 'StudentClass', foreignKey: 'studentNo'});
Class.belongsToMany(models.Student ,{through: 'StudentClass', foreignKey: 'classCode'});
このように多対多の場合は、belongsToMany()
の第二引数に{throgh: 連関テーブルテーブルのモデル}
という形で連関テーブル(junction table
とも)を指定する。(1対1
や1対多
と同様に外部キーも同時に指定する)
結合演算
ここからは、上で紹介したモデルとそれに対応したテーブルがDBで作成されていることを前提に進めていく。Sequelize-cli
によるテーブル作成についてはこちらを参照してほしいンゴ。(cliによるモデル定義時に作成されるマイグレーションファイルが存在していることが前提)
準備
シードデータの準備
まずは結合演算のためのシードデータも用意しておきたい。シードデータの作成の仕方についてはこちらを参照。以下に今回使うデータをあげておく。
//Teacher
[{
teacherNo: '001',
firstName: 'イチロー',
lastName: '鈴木'
},{
teacherNo: '002',
firstName: '秀喜',
lastName: '松井'
},{
teacherNo: '003',
firstName: '英雄',
lastName: '野茂'
},{
teacherNo: '004',
firstName: '久志',
lastName: '岩隈'
}];
//Faculty
[{
facultyNo: 'A',
facultyName: '経済',
deanNo: '001'
},{
facultyNo: 'B',
facultyName: '文学',
deanNo: '002'
},{
facultyNo: 'C',
facultyName: '法学',
deanNo: '003'
},{
facultyNo: 'D',
facultyName: '医学',
deanNo: '004'
}];
//Student
[{
studentNo: '001',
firstName: '翔平',
lastName: '大谷',
facultyNo: 'A'
},{
studentNo: '002',
firstName: '有',
lastName: 'ダルビッシュ',
facultyNo: 'B'
},{
studentNo: '003',
firstName: '雄星',
lastName: '菊池',
facultyNo: 'C'
},{
studentNo: '004',
firstName: '健太',
lastName: '前田',
facultyNo: 'A'
},{
studentNo: '005',
firstName: '拓一',
lastName: '沢村',
facultyNo: 'B'
},{
studentNo: '006',
firstName: '将大',
lastName: '田中',
facultyNo: 'C'
}];
//Class
[{
classCode: '001',
className: 'マクロ経済学',
teacherNo: '001'
},
{
classCode: '002',
className: 'ミクロ経済学',
teacherNo: '001'
},
{
classCode: '003',
className: '行動経済学',
teacherNo: '001'
},{
classCode: '004',
className: '公共政策学',
teacherNo: '001'
},
{
classCode: '005',
className: '英文学',
teacherNo: '002'
},
{
classCode: '006',
className: '日本文学',
teacherNo: '002'
},
{
classCode: '007',
className: '民法',
teacherNo: '003'
},
{
classCode: '008',
className: '刑法',
teacherNo: '003'
}];
//StudentClass
[{
studentNo: '001',
classCode: '001'
},{
studentNo: '001',
classCode: '002'
},{
studentNo: '001',
classCode: '003'
},{
studentNo: '001',
classCode: '004'
},{
studentNo: '004',
classCode: '002'
},{
studentNo: '004',
classCode: '003'
},{
studentNo: '002',
classCode: '005'
},{
studentNo: '002',
classCode: '006'
},{
studentNo: '005',
classCode: '005'
},
{
studentNo: '005',
classCode: '006'
},{
studentNo: '003',
classCode: '007'
}
,{
studentNo: '003',
classCode: '008'
}
,{
studentNo: '006',
classCode: '007'
},
{
studentNo: '006',
classCode: '008'
}];
APIサーバの構築
次に、以下のひな型を用意して、これにルーティングとかを追加する形で肉付けしていく。expressは別途npm install
しておきたい。
const express = require('express');
const app = express();
app.listen(3000, function() {
console.log('server running on port 3000...');
});
次にこのhoge.js
上で、今回作成したStudent
モデルを使えるようにしなきゃいけない。今回はSequelize-cli
を使って作成したmodels
以下のモデルファイルを利用するので、
const models = require('{modelsフォルダの相対パス}');
を追加する。
これで結合演算の結果を返すAPIを構築する準備が出来たので、APIを実行するとSequelizeによって特定の結合演算のクエリングが実行されるコードを実装していく。findAll()
の引数に{include: models.モデル名}
を渡している部分がポイント。これによってテーブル結合が実現される。
app.get('/class-student', (req, res) => {
models.Class.findAll({include: models.Student}).then((result) => {
if (!result) {
res.status(200).json({"message": "レコードが見つかりませんでした"});
} else {
res.send(result);
}
}).catch((err) => {
res.status(500).json({"error message": err.message});
});
});
app.get('/student-class', (req, res) => {
models.Student.findAll({include: models.Class}).then((result) => {
if (!result) {
res.status(200).json({"message": "レコードが見つかりませんでした"});
} else {
res.send(result);
}
}).catch((err) => {
res.status(500).json({"error message": err.message});
});
});
app.get('/class-teacher', (req, res) => {
models.Class.findAll({include: models.Teacher}).then((result) => {
if (!result) {
res.status(200).json({"message": "レコードが見つかりませんでした"});
} else {
res.send(result);
}
}).catch((err) => {
res.status(500).json({"error message": err.message});
});
});
app.get('/faculty-teacher', (req, res) => {
models.Faculty.findAll({include: models.Teacher}).then((result) => {
if (!result) {
res.status(200).json({"message": "レコードが見つかりませんでした"});
} else {
res.send(result);
}
}).catch((err) => {
res.status(500).json({"error message": err.message});
});
});
app.get('/student-faculty', (req, res) => {
models.Student.findAll({include: models.Faculty}).then((result) => {
if (!result) {
res.status(200).json({"message": "レコードが見つかりませんでした"});
} else {
res.send(result);
}
}).catch((err) => {
res.status(500).json({"error message": err.message});
});
});
実行
上記コードを追加したらnode hoge.js
でコードを実行してサーバを立ち上げ、実際にリクエストを送信してレスポンスを確認してみる。
例えば/faculty-student
APIを送信すると以下のようなレスポンスが返ってきて、設計通りFacultyに対してStudentが複数紐づいていることが確認できる。
[
{
"facultyNo": "A",
"deanNo": "001",
"facultyName": "経済",
"createdAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"updatedAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"deletedAt": null,
"Students": [
{
"studentNo": "001",
"firstName": "翔平",
"lastName": "大谷",
"createdAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"updatedAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"deletedAt": null,
"facultyNo": "A"
},
{
"studentNo": "004",
"firstName": "健太",
"lastName": "前田",
"createdAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"updatedAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"deletedAt": null,
"facultyNo": "A"
}
]
},
{
"facultyNo": "B",
"deanNo": "002",
"facultyName": "文学",
"createdAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"updatedAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"deletedAt": null,
"Students": [
{
"studentNo": "002",
"firstName": "有",
"lastName": "ダルビッシュ",
"createdAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"updatedAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"deletedAt": null,
"facultyNo": "B"
},
{
"studentNo": "005",
"firstName": "拓一",
"lastName": "沢村",
"createdAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"updatedAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"deletedAt": null,
"facultyNo": "B"
}
]
},
{
"facultyNo": "C",
"deanNo": "003",
"facultyName": "法学",
"createdAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"updatedAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"deletedAt": null,
"Students": [
{
"studentNo": "003",
"firstName": "雄星",
"lastName": "菊池",
"createdAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"updatedAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"deletedAt": null,
"facultyNo": "C"
},
{
"studentNo": "006",
"firstName": "将大",
"lastName": "田中",
"createdAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"updatedAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"deletedAt": null,
"facultyNo": "C"
}
]
},
{
"facultyNo": "D",
"deanNo": "004",
"facultyName": "医学",
"createdAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"updatedAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"deletedAt": null,
"Students": [
]
}
]
(createdAt
とupdatedAt
はマスキングしてるンゴ)
ここでは省略するけども、他のAPIについても実行してレスポンスをエンティティ間のリレーションとともに確認してみるとグッドんご。
内部結合と外部結合
左外部結合
ここで、上で/faculty-student
を実行した際に実際に発行されたSQLをサーバのログで確認すると以下のようになっている。
Executing (default): SELECT "Faculty"."facultyNo", "Faculty"."deanNo", "Faculty"."facultyName", "Faculty"."createdAt", "Faculty
"."updatedAt", "Faculty"."deletedAt", "Students"."studentNo" AS "Students.studentNo", "Students"."firstName" AS "Students.first
Name", "Students"."lastName" AS "Students.lastName", "Students"."createdAt" AS "Students.createdAt", "Students"."updatedAt" AS
"Students.updatedAt", "Students"."deletedAt" AS "Students.deletedAt", "Students"."facultyNo" AS "Students.facultyNo" FROM "Facu
lty" AS "Faculty" LEFT OUTER JOIN "Student" AS "Students" ON "Faculty"."facultyNo" = "Students"."facultyNo" AND ("Students"."de
letedAt" IS NULL) WHERE ("Faculty"."deletedAt" IS NULL);
実はinclude
はデフォルトでLEFT OUTER JOIN
(左外部結合)が使われることになっている。つまり、faculty
を基準にしてstudent
が結合されるため、学生が存在しない医学部(facultyNo
= D
)も実行結果のレコードとして含まれる。
右外部結合
右外部結合を行いたい場合は、明示的にそれを記述する必要がある。具体的には以下のように、finders系メソッドに渡していたinclude
の部分をinclude: {model: モデル名, right: true}
とする。
app.get('/student-faculty-right-outer-join', (req, res) => {
models.Student.findAll({include: {model: models.Faculty, right: true}}).then((result) => {
if (!result) {
res.status(200).json({"message": "レコードが見つかりませんでした"});
} else {
res.send(result);
}
}).catch((err) => {
res.status(500).json({"error message": err.message});
});
});
追加したstudent-faculty-right-outer-join
を実行してみると、以下のレスポンスが返ってくる。ちゃんと右外部結合になってる。
[
{
"studentNo": "004",
"firstName": "健太",
"lastName": "前田",
"createdAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"updatedAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"deletedAt": null,
"facultyNo": "A",
"Faculty": {
"facultyNo": "A",
"deanNo": "001",
"facultyName": "経済",
"createdAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"updatedAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"deletedAt": null
}
},
{
"studentNo": "001",
"firstName": "翔平",
"lastName": "大谷",
"createdAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"updatedAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"deletedAt": null,
"facultyNo": "A",
"Faculty": {
"facultyNo": "A",
"deanNo": "001",
"facultyName": "経済",
"createdAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"updatedAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"deletedAt": null
}
},
{
"studentNo": "005",
"firstName": "拓一",
"lastName": "沢村",
"createdAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"updatedAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"deletedAt": null,
"facultyNo": "B",
"Faculty": {
"facultyNo": "B",
"deanNo": "002",
"facultyName": "文学",
"createdAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"updatedAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"deletedAt": null
}
},
{
"studentNo": "002",
"firstName": "有",
"lastName": "ダルビッシュ",
"createdAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"updatedAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"deletedAt": null,
"facultyNo": "B",
"Faculty": {
"facultyNo": "B",
"deanNo": "002",
"facultyName": "文学",
"createdAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"updatedAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"deletedAt": null
}
},
{
"studentNo": "006",
"firstName": "将大",
"lastName": "田中",
"createdAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"updatedAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"deletedAt": null,
"facultyNo": "C",
"Faculty": {
"facultyNo": "C",
"deanNo": "003",
"facultyName": "法学",
"createdAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"updatedAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"deletedAt": null
}
},
{
"studentNo": "003",
"firstName": "雄星",
"lastName": "菊池",
"createdAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"updatedAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"deletedAt": null,
"facultyNo": "C",
"Faculty": {
"facultyNo": "C",
"deanNo": "003",
"facultyName": "法学",
"createdAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"updatedAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"deletedAt": null
}
},
{
"studentNo": null,
"firstName": null,
"lastName": null,
"createdAt": null,
"updatedAt": null,
"deletedAt": null,
"facultyNo": null,
"Faculty": {
"facultyNo": "D",
"deanNo": "004",
"facultyName": "医学",
"createdAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"updatedAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"deletedAt": null
}
}
]
内部結合
内部結合の場合も、明示的にそれを記述する必要がある。具体的には以下のように、finders系メソッドに渡していたinclude
の部分をinclude: {model: モデル名, required: true}
とする。
app.get('/student-faculty-inner-join', (req, res) => {
models.Student.findAll({include: {model: models.Faculty, required: true}}).then((result) => {
if (!result) {
res.status(200).json({"message": "レコードが見つかりませんでした"});
} else {
res.send(result);
}
}).catch((err) => {
res.status(500).json({"error message": err.message});
});
});
追加したstudent-faculty-inner-join
を実行してみると、以下のレスポンスが返ってくる。先ほどの右外部結合のレスポンスと違って、Student
側に存在しない医学部(facultyNo
= D
)が含まれていないので、ちゃんと内部結合になってることが分かる。
[
{
"studentNo": "001",
"firstName": "翔平",
"lastName": "大谷",
"createdAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"updatedAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"deletedAt": null,
"facultyNo": "A",
"Faculty": {
"facultyNo": "A",
"deanNo": "001",
"facultyName": "経済",
"createdAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"updatedAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"deletedAt": null
}
},
{
"studentNo": "002",
"firstName": "有",
"lastName": "ダルビッシュ",
"createdAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"updatedAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"deletedAt": null,
"facultyNo": "B",
"Faculty": {
"facultyNo": "B",
"deanNo": "002",
"facultyName": "文学",
"createdAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"updatedAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"deletedAt": null
}
},
{
"studentNo": "003",
"firstName": "雄星",
"lastName": "菊池",
"createdAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"updatedAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"deletedAt": null,
"facultyNo": "C",
"Faculty": {
"facultyNo": "C",
"deanNo": "003",
"facultyName": "法学",
"createdAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"updatedAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"deletedAt": null
}
},
{
"studentNo": "004",
"firstName": "健太",
"lastName": "前田",
"createdAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"updatedAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"deletedAt": null,
"facultyNo": "A",
"Faculty": {
"facultyNo": "A",
"deanNo": "001",
"facultyName": "経済",
"createdAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"updatedAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"deletedAt": null
}
},
{
"studentNo": "005",
"firstName": "拓一",
"lastName": "沢村",
"createdAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"updatedAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"deletedAt": null,
"facultyNo": "B",
"Faculty": {
"facultyNo": "B",
"deanNo": "002",
"facultyName": "文学",
"createdAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"updatedAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"deletedAt": null
}
},
{
"studentNo": "006",
"firstName": "将大",
"lastName": "田中",
"createdAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"updatedAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"deletedAt": null,
"facultyNo": "C",
"Faculty": {
"facultyNo": "C",
"deanNo": "003",
"facultyName": "法学",
"createdAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"updatedAt": "2023-MM-DDTHH:MM:SS.XXXZ",
"deletedAt": null
}
}
]