1. 项目概述:构建命令行待办事项应用
作为一个长期与终端打交道的开发者,我始终认为命令行工具是最高效的生产力伙伴。今天要分享的,是一个用Python和SQLAlchemy构建的纯命令行待办事项管理工具。这个项目特别适合那些喜欢键盘操作胜过鼠标点击的效率追求者,也适合作为学习数据库操作的实战案例。
这个工具的核心功能包括:
- 任务的增删改查(CRUD)
- 任务分类和优先级管理
- 简单的数据持久化存储
- 直观的命令行交互界面
选择SQLAlchemy作为ORM框架,主要是看中它的稳定性和灵活性。不同于直接写SQL语句,ORM让我们能用面向对象的方式操作数据库,这在复杂业务场景下优势尤为明显。接下来,我会从环境搭建开始,逐步展示如何实现这个工具。
2. 环境准备与SQLAlchemy基础
2.1 安装与配置
首先确保你的Python环境是3.6+版本,然后安装必要的依赖:
bash复制pip install sqlalchemy
如果是生产环境,建议根据数据库类型安装对应的驱动。我们这个演示项目使用SQLite就足够了,它无需额外安装驱动,数据存储在单个文件中,非常适合小型应用。
提示:虽然SQLite适合开发和小型应用,但如果预计数据量会很大(比如超过10万条记录),建议考虑PostgreSQL或MySQL。只需将驱动换成psycopg2或mysql-connector即可。
2.2 初始化数据库连接
创建database.py文件,设置数据库连接:
python复制from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base
# 使用SQLite内存数据库(开发用)
# engine = create_engine('sqlite:///:memory:', echo=True)
# 使用文件型SQLite数据库(生产用)
engine = create_engine('sqlite:///todo.db', echo=False)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
这里有几个关键点需要注意:
echo=True会在控制台打印所有SQL语句,调试时非常有用:memory:表示使用内存数据库,程序退出后数据消失- 生产环境应该使用文件型数据库(如
todo.db)
3. 数据模型设计
3.1 定义任务模型
在models.py中定义我们的核心模型:
python复制from datetime import datetime
from sqlalchemy import Column, Integer, String, Boolean, DateTime, Enum
from database import Base
import enum
class Priority(enum.Enum):
LOW = 1
MEDIUM = 2
HIGH = 3
class Task(Base):
__tablename__ = 'tasks'
id = Column(Integer, primary_key=True, index=True)
title = Column(String(100), nullable=False)
description = Column(String(500))
is_completed = Column(Boolean, default=False)
created_at = Column(DateTime, default=datetime.utcnow)
due_date = Column(DateTime, nullable=True)
priority = Column(Enum(Priority), default=Priority.MEDIUM)
category = Column(String(50), index=True)
def __repr__(self):
return f"<Task(id={self.id}, title='{self.title}')>"
这个模型设计考虑了以下实际需求:
- 任务标题是必填项(nullable=False)
- 使用枚举类型定义优先级,避免魔法数字
- 添加创建时间戳,方便排序和统计
- 类别字段加了索引,提高查询效率
3.2 创建数据库表
在项目启动时,我们需要确保表已经创建。在main.py中添加:
python复制from models import Task
from database import engine, Base
def init_db():
Base.metadata.create_all(bind=engine)
if __name__ == "__main__":
init_db()
print("数据库初始化完成!")
运行这个脚本后,会在当前目录生成todo.db文件。如果表结构有变更,可以删除这个文件重新运行。
4. 核心功能实现
4.1 任务管理类
创建一个task_manager.py来封装所有数据库操作:
python复制from sqlalchemy.orm import Session
from models import Task, Priority
from datetime import datetime
class TaskManager:
def __init__(self, session: Session):
self.session = session
def add_task(self, title: str, description: str = None,
due_date: datetime = None, priority: Priority = Priority.MEDIUM,
category: str = None):
"""添加新任务"""
task = Task(
title=title,
description=description,
due_date=due_date,
priority=priority,
category=category
)
self.session.add(task)
self.session.commit()
return task
def get_task(self, task_id: int):
"""获取单个任务"""
return self.session.query(Task).filter(Task.id == task_id).first()
def list_tasks(self, completed: bool = None, category: str = None):
"""列出所有任务,可筛选"""
query = self.session.query(Task)
if completed is not None:
query = query.filter(Task.is_completed == completed)
if category:
query = query.filter(Task.category == category)
return query.order_by(Task.priority.desc(), Task.due_date).all()
def complete_task(self, task_id: int):
"""标记任务为已完成"""
task = self.get_task(task_id)
if task:
task.is_completed = True
self.session.commit()
return task
def update_task(self, task_id: int, **kwargs):
"""更新任务信息"""
task = self.get_task(task_id)
if task:
for key, value in kwargs.items():
setattr(task, key, value)
self.session.commit()
return task
def delete_task(self, task_id: int):
"""删除任务"""
task = self.get_task(task_id)
if task:
self.session.delete(task)
self.session.commit()
return task
4.2 命令行界面
为了让普通用户也能方便使用,我们添加一个简单的命令行界面cli.py:
python复制import argparse
from datetime import datetime
from database import SessionLocal
from task_manager import TaskManager
from models import Priority
def main():
parser = argparse.ArgumentParser(description="命令行待办事项管理工具")
subparsers = parser.add_subparsers(dest="command", required=True)
# 添加任务
add_parser = subparsers.add_parser("add", help="添加新任务")
add_parser.add_argument("title", help="任务标题")
add_parser.add_argument("-d", "--description", help="任务描述")
add_parser.add_argument("--due-date", help="截止日期 (YYYY-MM-DD)")
add_parser.add_argument("-p", "--priority", choices=["low", "medium", "high"], default="medium")
add_parser.add_argument("-c", "--category", help="任务分类")
# 列出任务
list_parser = subparsers.add_parser("list", help="列出任务")
list_parser.add_argument("--completed", action="store_true", help="只显示已完成任务")
list_parser.add_argument("--pending", action="store_true", help="只显示未完成任务")
list_parser.add_argument("--category", help="按分类筛选")
# 完成任务
complete_parser = subparsers.add_parser("complete", help="标记任务为已完成")
complete_parser.add_argument("task_id", type=int, help="任务ID")
# 删除任务
delete_parser = subparsers.add_parser("delete", help="删除任务")
delete_parser.add_argument("task_id", type=int, help="任务ID")
args = parser.parse_args()
db = SessionLocal()
manager = TaskManager(db)
try:
if args.command == "add":
due_date = datetime.strptime(args.due_date, "%Y-%m-%d") if args.due_date else None
priority = Priority[args.priority.upper()]
task = manager.add_task(
title=args.title,
description=args.description,
due_date=due_date,
priority=priority,
category=args.category
)
print(f"任务添加成功!ID: {task.id}")
elif args.command == "list":
completed = None
if args.completed:
completed = True
elif args.pending:
completed = False
tasks = manager.list_tasks(completed=completed, category=args.category)
for task in tasks:
status = "✓" if task.is_completed else " "
print(f"{task.id}. [{status}] {task.title} (优先级: {task.priority.name}, 分类: {task.category})")
if task.description:
print(f" 描述: {task.description}")
if task.due_date:
print(f" 截止: {task.due_date.strftime('%Y-%m-%d')}")
elif args.command == "complete":
task = manager.complete_task(args.task_id)
if task:
print(f"任务 {task.id} 标记为已完成!")
else:
print("任务不存在!")
elif args.command == "delete":
task = manager.delete_task(args.task_id)
if task:
print(f"任务 {task.id} 已删除!")
else:
print("任务不存在!")
except Exception as e:
print(f"发生错误: {e}")
finally:
db.close()
if __name__ == "__main__":
main()
5. 使用示例与高级功能
5.1 基本操作示例
安装完成后,你可以这样使用这个工具:
bash复制# 添加任务
python cli.py add "完成项目报告" -d "需要包含数据分析部分" --due-date 2023-12-31 -p high -c work
# 列出所有任务
python cli.py list
# 列出工作分类的任务
python cli.py list --category work
# 标记任务为已完成
python cli.py complete 1
# 删除任务
python cli.py delete 1
5.2 添加搜索功能
让我们增强任务管理器的搜索能力。在TaskManager类中添加:
python复制def search_tasks(self, keyword: str):
"""搜索任务标题和描述中包含关键词的任务"""
return self.session.query(Task).filter(
(Task.title.ilike(f"%{keyword}%")) |
(Task.description.ilike(f"%{keyword}%"))
).all()
然后在CLI中添加对应的命令:
python复制# 在cli.py的subparsers中添加
search_parser = subparsers.add_parser("search", help="搜索任务")
search_parser.add_argument("keyword", help="搜索关键词")
# 在命令处理部分添加
elif args.command == "search":
tasks = manager.search_tasks(args.keyword)
for task in tasks:
print(f"{task.id}. {task.title}")
if task.description:
print(f" 描述: {task.description}")
5.3 数据统计功能
添加一些有用的统计功能:
python复制def get_stats(self):
"""获取任务统计信息"""
total = self.session.query(Task).count()
completed = self.session.query(Task).filter(Task.is_completed == True).count()
pending = total - completed
# 按优先级统计
priority_stats = {}
for priority in Priority:
count = self.session.query(Task).filter(Task.priority == priority).count()
priority_stats[priority.name] = count
# 按分类统计
category_stats = {}
categories = self.session.query(Task.category).distinct().all()
for (category,) in categories:
if category:
count = self.session.query(Task).filter(Task.category == category).count()
category_stats[category] = count
return {
"total": total,
"completed": completed,
"pending": pending,
"priority_stats": priority_stats,
"category_stats": category_stats
}
6. 性能优化与最佳实践
6.1 会话管理优化
我们之前的实现每次操作都会创建新会话,这在Web应用中不够高效。更好的做法是使用依赖注入:
python复制from contextlib import contextmanager
@contextmanager
def get_db():
db = SessionLocal()
try:
yield db
db.commit()
except Exception:
db.rollback()
raise
finally:
db.close()
# 使用示例
with get_db() as db:
manager = TaskManager(db)
tasks = manager.list_tasks()
6.2 批量操作优化
当需要处理大量数据时,单个提交会很慢。SQLAlchemy提供了批量操作API:
python复制def bulk_add_tasks(self, tasks_data: list):
"""批量添加任务"""
tasks = [
Task(
title=data["title"],
description=data.get("description"),
due_date=data.get("due_date"),
priority=data.get("priority", Priority.MEDIUM),
category=data.get("category")
) for data in tasks_data
]
self.session.bulk_save_objects(tasks)
self.session.commit()
6.3 索引优化
对于大型数据集,合理的索引能显著提高查询速度。除了模型定义中的索引,我们还可以:
python复制from sqlalchemy import Index
# 在模型定义后添加复合索引
Index('idx_task_category_priority', Task.category, Task.priority)
Index('idx_task_due_date', Task.due_date)
7. 常见问题与解决方案
7.1 数据库连接泄漏
症状:程序运行一段时间后变慢,甚至崩溃。
解决方案:
- 始终使用上下文管理器管理会话
- 确保在finally块中关闭会话
- 设置连接池参数:
python复制engine = create_engine(
'sqlite:///todo.db',
pool_size=5,
max_overflow=10,
pool_timeout=30,
pool_recycle=3600
)
7.2 N+1查询问题
症状:获取关联数据时产生大量查询。
解决方案:使用joinedload或selectinload:
python复制from sqlalchemy.orm import joinedload
# 假设我们有关联模型
tasks = session.query(Task).options(joinedload(Task.subtasks)).all()
7.3 日期时间处理
症状:时区问题导致日期显示不正确。
解决方案:
- 存储时统一使用UTC时间
- 在显示时转换为本地时间
python复制from datetime import datetime, timezone
# 存储时
Task.created_at = datetime.now(timezone.utc)
# 显示时
local_time = task.created_at.astimezone()
print(local_time.strftime('%Y-%m-%d %H:%M:%S'))
8. 项目扩展思路
这个基础版本还可以进一步扩展:
- 用户系统:添加用户模型,实现多用户支持
- 提醒功能:集成邮件或系统通知,提醒即将到期的任务
- 数据导出:支持将任务导出为CSV或JSON格式
- Web界面:使用FastAPI或Flask添加REST API
- 同步功能:通过云存储实现多设备同步
例如,添加用户认证可以这样修改模型:
python复制class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
username = Column(String(50), unique=True)
password_hash = Column(String(128))
tasks = relationship("Task", back_populates="user")
class Task(Base):
# ...原有字段...
user_id = Column(Integer, ForeignKey('users.id'))
user = relationship("User", back_populates="tasks")
这个命令行待办事项应用虽然简单,但涵盖了SQLAlchemy的大部分核心功能。从模型定义到复杂查询,从事务管理到性能优化,这些都是实际项目中经常遇到的场景。