Sequelizeで複数テーブルのリレーションを定義し、テーブル結合してみる

# プロローグ
　今回はSequelizeでエンティティ間のリレーションを定義する方法と、定義したリレーションに基づいて結合演算をする方法を紹介していくンゴ。結合演算はこのあと出てくるER図のエンティティ、リレーションを対象に行う。また、リクエストを送信すると結合演算の結果をjson形式で返すAPIサーバを構築し、レスポンスを確認する形で結合演算の結果を確認していく。

# 開発環境
* `sequelize: 6.21.0`
* `sequelize-cli: 6.4.1`
* `node: 17.1.0`
* `express: 4.18.1`

# 今回定義するエンティティとリレーション
![er.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2765600/baedb374-b88c-58f5-6fa1-9e02a0472650.png)

* `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`で作成したモデルファイルを利用しているので、それらに対してリレーションに関する記述を追加していく。<br><br>　具体的には、各モデルをcliで作成した際に作成されるモデルファイルの`// define association here`とあるコメント部分に追加することになる。
```js
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
    }
}
```
　以下はリレーションに関する記述を追加した後のモデルファイル。

``` class.js
'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;
};
```

``` faculty.js
'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;
};
```

``` student.js
'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;
};
```

``` studentClass.js
'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;
};
```

``` teacher.js
'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;
};
```
以上のように、リレーションは各モデルファイルの
``` js
static associate(models) {}
```
の部分に記載していて、他のモデルを参照する際は引数の`models`を使って`models.Hoge`と記述する。具体的なリレーションの定義の仕方は以下の通り
* 1対1
``` js
Teacher.hasOne(models.Faculty, {foreignKey: 'deanNo'});
Faculty.belongsTo(models.Teacher, {foreignKey: 'deanNo'});
```
`{foreignKey: 'deanNo'}`で実際の外部キーを指定しているのがポイント。これは`1対多`や`多対多`のリレーションでも同じ。これがないとSequelizeが勝手に外部キーを予測してしまって、結果的に存在しないカラムを参照して、実行時にエラーになってしまう。
* 1対多
``` js
//1の側
Student.belongsTo(models.Faculty, {foreignKey: 'facultyNo'});
//多の側
Faculty.hasMany(models.Student, {foreignKey: 'facultyNo'});
```

* 多対多
``` js
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`によるテーブル作成については[こちら](https://qiita.com/furi-kake/items/c47b098cdcf053bcf287#%E3%83%86%E3%83%BC%E3%83%96%E3%83%AB%E3%81%AE%E4%BD%9C%E6%88%90)を参照してほしいンゴ。（cliによるモデル定義時に作成されるマイグレーションファイルが存在していることが前提）

## 準備
### シードデータの準備
　まずは結合演算のためのシードデータも用意しておきたい。シードデータの作成の仕方については[こちら](https://qiita.com/furi-kake/items/c47b098cdcf053bcf287#%E3%83%87%E3%83%BC%E3%82%BF%E3%81%AE%E6%8C%BF%E5%85%A5%E6%BA%96%E5%82%99)を参照。以下に今回使うデータをあげておく。
``` js
//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`しておきたい。
``` hoge.js
const express = require('express');
const app = express();

app.listen(3000, function() {
    console.log('server running on port 3000...');
});
```
次にこの`hoge.js`上で、今回作成した`Student`モデルを使えるようにしなきゃいけない。今回は`Sequelize-cli`を使って作成した`models`以下のモデルファイルを利用するので、
```js
const models = require('{modelsフォルダの相対パス}');
```
を追加する。

これで結合演算の結果を返すAPIを構築する準備が出来たので、APIを実行するとSequelizeによって特定の結合演算のクエリングが実行されるコードを実装していく。`findAll()`の引数に`{include: models.モデル名}`を渡している部分がポイント。これによってテーブル結合が実現される。

```js
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}`とする。

```js
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}`とする。
```js
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
        }
    }
]
```