## 一、外键（ForeignKey）核心概念

### 1. 什么是外键

- **外键 = 外部码（Foreign Key）**，是一个表中的字段，引用另一个表的主键，用来建立表之间的关系
    
- 作用：建立表关联、保证数据完整性（参照完整性）、支持级联操作
    
- 命名来源：因为值来自**另一个表（外部）**，不是当前表自己生成的
    

### 2. 外键放在"多"的一方

- **一对多**关系（如 1 个班级 → 多个学生），外键放在"多"的一方（学生表）
    
- 原因：一个学生只属于 1 个班级，学生表存 `class_id` 合理；反过来则不符合逻辑
    

### 3. on_delete 选项

|选项|含义|
|---|---|
|CASCADE|级联删除（班级删除，学生也删除）|
|PROTECT|禁止删除（有学生时删班级会报错）|
|SET_NULL|外键设为 NULL（需 null=True）|
|SET_DEFAULT|设为默认值（需 default=...）|

### 4. 列名可以不同

- 外键列 `publish_id` 关联目标表 `Publish.id`，**列名不同也能正确关联**
    
- 数据库关联的本质是**值匹配**，不是列名匹配
    
- `publish_id` 是数据库存储的实际外键，`publish` 是 Django 提供的对象访问接口
    

---

## 二、正向查询 vs 反向查询

### 1. 核心规则

|关联类型|正向查询|反向查询|
|---|---|---|
|ForeignKey| `book.publish`（单个对象）| `publisher.book_set.all()` |
|ManyToManyField| `book.authors.all()` | `author.book_set.all()` |
|OneToOneField| `profile.user` | `user.profile`（无 _set）|

### 2. 正反向由**字段定义位置**决定

- **正向**：从**定义字段的模型**出发访问关联模型
    
- **反向**：从**被关联的模型**出发访问定义方
    
- 例：`Book` 中定义 `authors = ManyToManyField(Author)` → `book.authors` 是正向，`author.book_set` 是反向
    
- 若改为在 `Author` 中定义 `books = ManyToManyField(Book)` → 正反向就反转了
    

### 3. related_name 自定义反向名

python

author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name="books")
# 之后用 author.books.all() 代替 author.book_set.all()

### 4. 多对多的底层实现

- 多对多会**自动创建中间表**（如 `book_authors`），存储 `book_id` 和 `author_id` 两个外键
    
- 没有 `author_id` 直接存在 `Book` 表中，正反向查询都通过操作中间表实现
    

---

## 三、关联管理器 .add() vs .create()

|特性|.add()|.create()|
|---|---|---|
|作用|关联**已存在**的对象|**创建新对象**并关联|
|是否创建新对象|❌ 否|✅ 是|
|参数类型|对象或 ID|新对象的字段键值对|
|返回值|无|新创建的对象|
|适用|ManyToManyField、ForeignKey|ManyToManyField、ForeignKey 的反向查询|

---

## 四、SQL 对应关系

### 示例代码

python

book = models.Book.objects.filter(title="菜鸟教程").first()
res = book.authors.all()
for i in res:
    print(i.name, i.au_detail.tel)

### 对应 SQL（假设 book.id=5，作者 id 为 10、11）

sql

-- 1. 查询书籍
SELECT * FROM book WHERE title = '菜鸟教程' LIMIT 1;
-- 2. 查询关联作者（通过中间表）
SELECT author.* FROM author
INNER JOIN book_authors ON author.id = book_authors.author_id
WHERE book_authors.book_id = 5;
-- 3. 循环查询作者详情（N+1 问题！）
SELECT * FROM author_detail WHERE author_id = 10 LIMIT 1;
SELECT * FROM author_detail WHERE author_id = 11 LIMIT 1;

### 优化方案（避免 N+1）

使用 `prefetch_related('authors__au_detail')`，将详情查询合并为：

sql

SELECT * FROM author_detail WHERE author_id IN (10, 11);

### 关于 book_id 的动态性

- `book_id` 的值来自 `book.id`，**不是固定的 1**，Django ORM 会自动使用实际主键值
    
- 若手动写 SQLite，需要分步查询或使用 JOIN/IN 动态处理
    

---

## 五、迁移与数据库问题

### 1. makemigrations 后数据库没出现新表

排查步骤：

1. **没有执行 migrate**（最常见）—— makemigrations 只生成迁移文件，需再运行 `python manage.py migrate`
    
2. 检查 `INSTALLED_APPS` 是否包含 App
    
3. 检查 `migrations` 文件夹是否存在迁移文件
    
4. 检查数据库连接配置
    
5. 检查表名是否正确（默认格式：`<app名>_<模型名小写>`）
    
6. 检查是否使用了自定义数据库路由
    

### 2. 表已存在错误

**错误**：`(1050, "Table 'testmodel_contact' already exists")`

解决方案：

- **方法 1（推荐）**：删除旧表 → 重新 migrate
    
- **方法 2**：`python manage.py migrate --fake-initial`（跳过已存在表）
    
- **方法 3**：修改模型 `Meta.db_table` 使用新表名
    
- **方法 4**：重置迁移记录（谨慎）
    
- **方法 5**：手动重命名旧表
    

### 3. 表不存在错误

**错误**：`(1051, "Unknown table 'runoob.testmodel_test'")` 或 `(1146, "Table 'runoob.testmodel_tag' doesn't exist")`

原因：

- 表确实被删除了
    
- 数据库和 Django 迁移不同步
    
- 模型被删除或改名，但旧迁移仍引用
    

解决方案：

- 重新生成迁移 + migrate
    
- `migrate --fake` 修复迁移记录
    
- 检查模型定义
    

### 4. 关于 Tag 表不存在的特殊情况

用户提到 models.py 中并没有 Tag 类，但报错提示 `testmodel_tag` 不存在——这通常是因为**旧的迁移文件仍引用已删除的模型**，需要清理迁移文件或修复迁移记录。

---

## 六、其他知识点

### 1. 主键 id 起始值

- SQLite、MySQL、PostgreSQL 默认都从 **1** 开始自增
    

### 2. MySQL longtext 索引警告

**警告**：`fields.W162 MySQL does not support a database index on longtext columns`

原因：

- MySQL 不支持直接对 `LONGTEXT` 类型列创建普通索引
    
- B-Tree 索引要求列长度不超过 3072 字节
    

解决方案：

- 移除 `db_index=True`（不需要索引时）
    
- 改用 `CharField(max_length=255)`
    
- 使用全文索引（FULLTEXT）
    
- 自定义迁移创建**前缀索引**：`CREATE INDEX idx ON table (question(255))`
    
- 沉默警告：`SILENCED_SYSTEM_CHECKS = ["fields.W162"]`