MySQL 外键详解
1. 外键概念
外键(Foreign Key)是数据库中用于建立表与表之间关系的约束机制,它指向另一个表的主键或唯一键。
2. 外键的作用
数据完整性保证
- 参照完整性:确保外键值在参照表中存在
- 级联操作:自动维护相关数据的一致性
- 约束检查:防止插入无效的关联数据
关系维护
- 一对多关系:一个父记录对应多个子记录
- 多对多关系:通过中间表实现
- 一对一关系:特殊的一对多关系
3. MySQL 外键语法
创建表时定义外键
-- 父表
CREATE TABLE departments (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 子表(带外键)
CREATE TABLE employees (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
email VARCHAR(100) UNIQUE,
department_id INT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-- 定义外键约束
CONSTRAINT fk_employee_department
FOREIGN KEY (department_id)
REFERENCES departments(id)
ON DELETE SET NULL
ON UPDATE CASCADE
);
添加外键约束
-- 为已存在的表添加外键
ALTER TABLE employees
ADD CONSTRAINT fk_employee_department
FOREIGN KEY (department_id)
REFERENCES departments(id)
ON DELETE CASCADE
ON UPDATE CASCADE;
删除外键约束
-- 删除外键约束
ALTER TABLE employees
DROP FOREIGN KEY fk_employee_department;
4. 外键约束选项
ON DELETE 选项
-- CASCADE: 删除父记录时自动删除相关子记录
ON DELETE CASCADE
-- SET NULL: 删除父记录时将外键字段设为NULL
ON DELETE SET NULL
-- RESTRICT: 有子记录时禁止删除父记录(默认)
ON DELETE RESTRICT
-- NO ACTION: 同RESTRICT
ON DELETE NO ACTION
-- SET DEFAULT: 设置为默认值(MySQL不支持)
ON UPDATE 选项
-- CASCADE: 更新父键时自动更新外键
ON UPDATE CASCADE
-- SET NULL: 更新父键时将外键设为NULL
ON UPDATE SET NULL
-- RESTRICT: 有子记录时禁止更新父键
ON UPDATE RESTRICT
-- NO ACTION: 同RESTRICT
ON UPDATE NO ACTION
5. 外键使用示例
完整的多表设计
-- 用户表
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 分类表
CREATE TABLE categories (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 文章表
CREATE TABLE articles (
id INT PRIMARY KEY AUTO_INCREMENT,
title VARCHAR(200) NOT NULL,
content TEXT,
author_id INT NOT NULL,
category_id INT,
status ENUM('draft', 'published', 'archived') DEFAULT 'draft',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-- 外键约束
CONSTRAINT fk_article_author
FOREIGN KEY (author_id)
REFERENCES users(id)
ON DELETE CASCADE
ON UPDATE CASCADE,
CONSTRAINT fk_article_category
FOREIGN KEY (category_id)
REFERENCES categories(id)
ON DELETE SET NULL
ON UPDATE CASCADE
);
-- 标签表
CREATE TABLE tags (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 文章标签关联表(多对多)
CREATE TABLE article_tags (
article_id INT,
tag_id INT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (article_id, tag_id),
CONSTRAINT fk_article_tag_article
FOREIGN KEY (article_id)
REFERENCES articles(id)
ON DELETE CASCADE
ON UPDATE CASCADE,
CONSTRAINT fk_article_tag_tag
FOREIGN KEY (tag_id)
REFERENCES tags(id)
ON DELETE CASCADE
ON UPDATE CASCADE
);
-- 评论表
CREATE TABLE comments (
id INT PRIMARY KEY AUTO_INCREMENT,
content TEXT NOT NULL,
article_id INT NOT NULL,
author_id INT NOT NULL,
parent_id INT DEFAULT NULL, -- 自引用外键,用于回复评论
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_comment_article
FOREIGN KEY (article_id)
REFERENCES articles(id)
ON DELETE CASCADE
ON UPDATE CASCADE,
CONSTRAINT fk_comment_author
FOREIGN KEY (author_id)
REFERENCES users(id)
ON DELETE CASCADE
ON UPDATE CASCADE,
CONSTRAINT fk_comment_parent
FOREIGN KEY (parent_id)
REFERENCES comments(id)
ON DELETE CASCADE
ON UPDATE CASCADE
);
6. 外键查询操作
基本联接查询
-- 内连接:获取有分类的文章
SELECT a.title, a.content, c.name as category_name, u.username as author
FROM articles a
INNER JOIN categories c ON a.category_id = c.id
INNER JOIN users u ON a.author_id = u.id;
-- 左连接:获取所有文章(包括无分类的)
SELECT a.title, c.name as category_name
FROM articles a
LEFT JOIN categories c ON a.category_id = c.id;
-- 多表连接
SELECT
a.title,
u.username as author,
c.name as category,
GROUP_CONCAT(t.name) as tags
FROM articles a
LEFT JOIN users u ON a.author_id = u.id
LEFT JOIN categories c ON a.category_id = c.id
LEFT JOIN article_tags at ON a.id = at.article_id
LEFT JOIN tags t ON at.tag_id = t.id
GROUP BY a.id;
子查询示例
-- 查找没有文章的分类
SELECT * FROM categories
WHERE id NOT IN (
SELECT DISTINCT category_id
FROM articles
WHERE category_id IS NOT NULL
);
-- 查找文章数量最多的用户
SELECT u.username, COUNT(a.id) as article_count
FROM users u
LEFT JOIN articles a ON u.id = a.author_id
GROUP BY u.id
ORDER BY article_count DESC
LIMIT 1;
Django 外键详解
1. Django 外键基础
Django 使用 ForeignKey 字段来定义外键关系,它会自动在数据库中创建外键约束。
2. 基本外键定义
简单外键关系
# models.py
from django.db import models
from django.contrib.auth.models import User
class Category(models.Model):
name = models.CharField(max_length=100)
description = models.TextField(blank=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name_plural = "Categories"
def __str__(self):
return self.name
class Article(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()
author = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name='articles'
)
category = models.ForeignKey(
Category,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='articles'
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.title
3. on_delete 参数详解
Django 的 on_delete 参数对应 MySQL 的 ON DELETE 选项:
from django.db import models
class Author(models.Model):
name = models.CharField(max_length=100)
class Book(models.Model):
title = models.CharField(max_length=200)
# CASCADE: 删除作者时删除所有相关书籍
author_cascade = models.ForeignKey(
Author,
on_delete=models.CASCADE
)
# SET_NULL: 删除作者时将外键设为 NULL
author_set_null = models.ForeignKey(
Author,
on_delete=models.SET_NULL,
null=True,
blank=True
)
# PROTECT: 有相关书籍时禁止删除作者
author_protect = models.ForeignKey(
Author,
on_delete=models.PROTECT
)
# SET_DEFAULT: 删除作者时设为默认值
author_set_default = models.ForeignKey(
Author,
on_delete=models.SET_DEFAULT,
default=1
)
# SET(): 删除时调用函数设置值
def get_default_author():
return Author.objects.get_or_create(name="Unknown")[0]
author_set_function = models.ForeignKey(
Author,
on_delete=models.SET(get_default_author)
)
# DO_NOTHING: 不做任何操作(可能导致数据库约束错误)
author_do_nothing = models.ForeignKey(
Author,
on_delete=models.DO_NOTHING
)
4. 外键字段选项
class Article(models.Model):
# 基本外键
category = models.ForeignKey(
'Category', # 可以使用字符串引用
on_delete=models.CASCADE
)
# 带完整选项的外键
author = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name='authored_articles', # 反向关系名称
related_query_name='authored_article', # 反向查询名称
to_field='username', # 指定引用字段(默认是主键)
db_column='author_username', # 数据库列名
db_index=True, # 创建数据库索引
limit_choices_to={'is_active': True}, # 限制选择范围
verbose_name='文章作者', # 字段显示名称
help_text='选择文章的作者' # 帮助文本
)
5. 关系查询
正向查询(从子对象到父对象)
# 获取文章的作者
article = Article.objects.get(id=1)
author = article.author
print(author.username)
# 获取文章的分类
category = article.category
if category:
print(category.name)
反向查询(从父对象到子对象)
# 获取用户的所有文章
user = User.objects.get(username='john')
articles = user.articles.all() # 使用 related_name
# 或者使用默认的反向关系
articles = user.article_set.all()
# 获取分类下的所有文章
category = Category.objects.get(name='Technology')
articles = category.articles.all()
跨关系查询
# 使用双下划线进行跨关系查询
# 查找特定作者的文章
articles = Article.objects.filter(author__username='john')
# 查找特定分类下的文章
articles = Article.objects.filter(category__name='Technology')
# 复杂的跨关系查询
articles = Article.objects.filter(
author__is_active=True,
category__name__icontains='tech',
created_at__year=2023
)
# 反向跨关系查询
users = User.objects.filter(articles__category__name='Technology')
# 统计查询
from django.db.models import Count
users_with_article_count = User.objects.annotate(
article_count=Count('articles')
).filter(article_count__gt=5)
6. 多对多关系
class Tag(models.Model):
name = models.CharField(max_length=50, unique=True)
def __str__(self):
return self.name
class Article(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()
author = models.ForeignKey(User, on_delete=models.CASCADE)
# 多对多关系
tags = models.ManyToManyField(
Tag,
blank=True,
related_name='articles',
through='ArticleTag' # 可选:指定中间表
)
# 自定义中间表
class ArticleTag(models.Model):
article = models.ForeignKey(Article, on_delete=models.CASCADE)
tag = models.ForeignKey(Tag, on_delete=models.CASCADE)
added_by = models.ForeignKey(User, on_delete=models.CASCADE)
added_at = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = ('article', 'tag')
# 多对多关系操作
article = Article.objects.get(id=1)
# 添加标签
tag = Tag.objects.get(name='Python')
article.tags.add(tag)
# 批量添加
article.tags.add(tag1, tag2, tag3)
# 删除标签
article.tags.remove(tag)
# 清空所有标签
article.tags.clear()
# 设置标签(替换所有现有标签)
article.tags.set([tag1, tag2])
# 查询
articles_with_python_tag = Article.objects.filter(tags__name='Python')
7. 自引用外键
class Comment(models.Model):
content = models.TextField()
article = models.ForeignKey(Article, on_delete=models.CASCADE)
author = models.ForeignKey(User, on_delete=models.CASCADE)
# 自引用外键,用于回复评论
parent = models.ForeignKey(
'self',
on_delete=models.CASCADE,
null=True,
blank=True,
related_name='replies'
)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"Comment by {self.author.username}"
# 使用自引用外键
# 创建主评论
main_comment = Comment.objects.create(
content="Great article!",
article=article,
author=user
)
# 创建回复
reply = Comment.objects.create(
content="I agree!",
article=article,
author=another_user,
parent=main_comment
)
# 查询评论的回复
replies = main_comment.replies.all()
# 查询顶级评论(没有父评论的评论)
top_level_comments = Comment.objects.filter(parent=None)
8. 外键性能优化
使用 select_related
# 避免 N+1 查询问题
# 不好的做法
articles = Article.objects.all()
for article in articles:
print(article.author.username) # 每次都会查询数据库
# 好的做法
articles = Article.objects.select_related('author', 'category').all()
for article in articles:
print(article.author.username) # 不会额外查询数据库
使用 prefetch_related
# 对于反向关系和多对多关系
authors = User.objects.prefetch_related('articles').all()
for author in authors:
for article in author.articles.all(): # 不会额外查询
print(article.title)
# 对于多对多关系
articles = Article.objects.prefetch_related('tags').all()
for article in articles:
for tag in article.tags.all():
print(tag.name)
使用 Prefetch 对象进行高级预取
from django.db.models import Prefetch
# 只预取已发布的文章
authors = User.objects.prefetch_related(
Prefetch(
'articles',
queryset=Article.objects.filter(status='published'),
to_attr='published_articles'
)
).all()
for author in authors:
for article in author.published_articles:
print(article.title)
9. 外键字段的表单处理
在 Django Forms 中使用外键
from django import forms
from .models import Article, Category
class ArticleForm(forms.ModelForm):
class Meta:
model = Article
fields = ['title', 'content', 'category', 'tags']
widgets = {
'category': forms.Select(attrs={'class': 'form-control'}),
'tags': forms.CheckboxSelectMultiple(),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# 限制分类选择
self.fields['category'].queryset = Category.objects.filter(
is_active=True
)
# 在视图中使用
from django.shortcuts import render, redirect
def create_article(request):
if request.method == 'POST':
form = ArticleForm(request.POST)
if form.is_valid():
article = form.save(commit=False)
article.author = request.user
article.save()
form.save_m2m() # 保存多对多关系
return redirect('article_detail', pk=article.pk)
else:
form = ArticleForm()
return render(request, 'create_article.html', {'form': form})
10. 外键的 Django Admin 配置
from django.contrib import admin
from .models import Article, Category, Tag
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
list_display = ['name', 'created_at']
search_fields = ['name']
class ArticleTagInline(admin.TabularInline):
model = Article.tags.through
extra = 1
@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
list_display = ['title', 'author', 'category', 'created_at']
list_filter = ['category', 'created_at', 'author']
search_fields = ['title', 'content']
raw_id_fields = ['author'] # 对于大量数据使用原始ID字段
filter_horizontal = ['tags'] # 多对多字段的水平过滤器
inlines = [ArticleTagInline]
def get_queryset(self, request):
# 优化查询
return super().get_queryset(request).select_related(
'author', 'category'
).prefetch_related('tags')
11. 数据库迁移
# 创建迁移文件
# python manage.py makemigrations
# 应用迁移
# python manage.py migrate
# 如果需要修改外键约束
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('myapp', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='article',
name='category',
field=models.ForeignKey(
blank=True,
null=True,
on_delete=models.SET_NULL,
related_name='articles',
to='myapp.category'
),
),
]
12. 最佳实践
命名规范
class Article(models.Model):
# 使用有意义的 related_name
author = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name='authored_articles' # 而不是默认的 'article_set'
)
# 对于可能为空的外键,总是设置 null=True, blank=True
category = models.ForeignKey(
Category,
on_delete=models.SET_NULL,
null=True,
blank=True
)
性能考虑
# 使用数据库级别的约束
class Article(models.Model):
author = models.ForeignKey(
User,
on_delete=models.CASCADE,
db_index=True # 创建数据库索引
)
class Meta:
indexes = [
models.Index(fields=['author', 'created_at']), # 复合索引
]
数据完整性
from django.core.exceptions import ValidationError
class Article(models.Model):
author = models.ForeignKey(User, on_delete=models.CASCADE)
def clean(self):
# 自定义验证
if self.author and not self.author.is_active:
raise ValidationError('Cannot assign inactive author')
def save(self, *args, **kwargs):
self.full_clean() # 调用验证方法
super().save(*args, **kwargs)