4、核心数据模型与ORM:用SQLAlchemy定义设备、租户、策略
数据模型这东西,说白了就是系统的骨架。你想想看,设备注册、身份管理、策略下发——所有功能都绕不开几个核心实体。我这些年做物联网平台,见过太多项目前期图省事,模型设计得稀里糊涂,结果后面改得痛不欲生。
今天我们就来把这几个核心模型理清楚。我会用Python的SQLAlchemy做演示,但思路是通用的,你用GORM或者其他ORM也一样。
4.1 先画一张关系图
动手写代码之前,我习惯先画一张图。把实体之间的关系看清楚,代码才不会写歪。
核心实体一览:
- 租户(Tenant) —— 隔离单位。一个企业就是一个租户。
- 设备(Device) —— 被管理的终端。每个设备属于一个租户。
- 策略(Policy) —— 控制规则。可以绑定到租户或设备。
- 注册凭证(Credential) —— 设备身份证明。每个设备可以有多个凭证。
我的经验: 别把关系搞得太复杂。我见过有人把设备分组、设备标签、设备属性全做成独立表,结果查询时关联七八张表,性能一塌糊涂。初期模型尽量精简,后面再按需扩展。
4.2 租户模型:隔离的起点
租户模型是整个系统的隔离边界。每个租户的数据必须严格分开,不能串。
from sqlalchemy import Column, String, DateTime, Enum, Text
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.sql import func
import uuid
class Tenant(Base):
__tablename__ = 'tenants'
tenant_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
name = Column(String(128), nullable=False, unique=True)
display_name = Column(String(256), nullable=False)
status = Column(Enum('active', 'suspended', 'deleted', name='tenant_status'),
default='active', nullable=False)
description = Column(Text, default='')
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
def __repr__(self):
return f"<Tenant(name='{self.name}', status='{self.status}')>"
这里有几个细节我特别想强调:
- 用UUID做主键 —— 自增ID在分布式场景下容易冲突,UUID虽然占空间大一点,但省心。我在一个项目里吃过自增ID的亏,两个数据中心的数据合并时ID全乱了。
- status字段用枚举 —— 别用字符串随便填,否则后面查数据时你会看到"active"、"Active"、"actived"各种写法,想死的心都有。
- 时间戳带时区 —— 设备可能分布在全球,不带时区的时间戳就是给自己挖坑。
4.3 设备模型:身份的核心
设备模型是系统里数据量最大的表。一台设备就是一个被管理的终端,它得有唯一的身份标识。
class Device(Base):
__tablename__ = 'devices'
device_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
tenant_id = Column(UUID(as_uuid=True),
ForeignKey('tenants.tenant_id', ondelete='CASCADE'),
nullable=False)
device_name = Column(String(128), nullable=False)
device_secret = Column(String(256), nullable=False) # 设备密钥,哈希存储
status = Column(Enum('online', 'offline', 'disabled', 'revoked',
name='device_status'),
default='offline', nullable=False)
firmware_version = Column(String(64), default='')
last_seen_at = Column(DateTime(timezone=True), nullable=True)
registered_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# 关系
tenant = relationship('Tenant', back_populates='devices')
credentials = relationship('Credential', back_populates='device',
cascade='all, delete-orphan')
__table_args__ = (
Index('idx_devices_tenant_status', 'tenant_id', 'status'),
)
⚠️ 注意: device_secret 绝对不能存明文!一定要用 bcrypt 或 argon2 做哈希。我曾经接手过一个项目,设备密钥直接明文存数据库,安全审计时直接被判不合格,整个平台被迫下线整改。
为什么要有 last_seen_at?说白了就是心跳。设备上次活跃时间能帮你判断它是不是还活着。我习惯在设备上线时更新这个字段,配合定时任务清理长期离线的设备。
4.4 策略模型:谁可以做什么
策略就是权限规则。它决定了某个设备能不能执行某个操作。我见过最简单的策略模型只有三个字段:主体、动作、效果。
class Policy(Base):
__tablename__ = 'policies'
policy_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
tenant_id = Column(UUID(as_uuid=True),
ForeignKey('tenants.tenant_id', ondelete='CASCADE'),
nullable=False)
policy_name = Column(String(128), nullable=False)
effect = Column(Enum('allow', 'deny', name='policy_effect'), nullable=False)
action = Column(String(128), nullable=False) # 例如 "device:read", "data:publish"
resource = Column(String(256), nullable=False) # 例如 "device/*", "topic/sensor/#"
priority = Column(Integer, default=0) # 数字越小优先级越高
description = Column(Text, default='')
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# 多对多关系:策略可以绑定到多个设备
devices = relationship('Device', secondary='policy_bindings',
back_populates='policies')
这里有个设计取舍:策略是绑定到租户级别还是设备级别?我的建议是——租户级别做默认策略,设备级别做例外策略。大部分规则在租户层面统一配置,只有特殊设备才单独绑定策略。这样策略数量可控,不会爆炸。
4.5 凭证模型:设备的身份证
凭证是设备用来证明自己身份的东西。可以是预置的密钥,也可以是动态颁发的令牌。
class Credential(Base):
__tablename__ = 'credentials'
cred_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
device_id = Column(UUID(as_uuid=True),
ForeignKey('devices.device_id', ondelete='CASCADE'),
nullable=False)
cred_type = Column(Enum('pre_shared_key', 'x509_cert', 'jwt_token',
name='credential_type'),
nullable=False)
cred_value = Column(Text, nullable=False) # 哈希后的凭证值
expires_at = Column(DateTime(timezone=True), nullable=True)
is_revoked = Column(Boolean, default=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
device = relationship('Device', back_populates='credentials')
我的习惯: 凭证一定要有过期时间。哪怕是预共享密钥,也建议定期轮换。我在一个车联网项目里,设备出厂时烧录的密钥用了三年没换过,结果某批次密钥泄露,几十万台设备全部需要远程更新凭证,那场面……别提了。
4.6 多对多关系:策略绑定表
策略和设备是多对多的关系。一张策略可以绑定多个设备,一个设备也可以应用多个策略。我们需要一张关联表。
class PolicyBinding(Base):
__tablename__ = 'policy_bindings'
binding_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
policy_id = Column(UUID(as_uuid=True),
ForeignKey('policies.policy_id', ondelete='CASCADE'),
nullable=False)
device_id = Column(UUID(as_uuid=True),
ForeignKey('devices.device_id', ondelete='CASCADE'),
nullable=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
__table_args__ = (
UniqueConstraint('policy_id', 'device_id', name='uq_policy_device'),
)
加唯一约束是为了防止重复绑定。同一个策略对同一个设备绑定两次,没有任何意义。
4.7 数据库迁移:让模型落地
模型定义好了,怎么让它变成数据库里的表?用Alembic。它是SQLAlchemy官方推荐的迁移工具。
初始化迁移环境:
# 安装
pip install alembic
# 初始化
alembic init migrations
配置 alembic.ini 里的数据库连接串:
sqlalchemy.url = postgresql://user:password@localhost:5432/device_registry
然后修改 migrations/env.py,导入你的模型:
from app.models import Base # 你的模型基类
target_metadata = Base.metadata
生成第一个迁移脚本:
alembic revision --autogenerate -m "init core tables"
检查生成的脚本,确认无误后执行:
alembic upgrade head
⚠️ 注意: --autogenerate 不是万能的。它检测不了所有变更,比如列名重命名、数据类型变更。每次自动生成后,一定要人工审查生成的脚本。我曾经因为没检查,自动迁移把一整个表的数据类型改错了,生产环境挂了半小时。
4.8 模型设计的几个原则
最后,分享几个我这些年总结出来的模型设计原则:
| 原则 | 说明 | 反面案例 |
|---|---|---|
| 单一职责 | 一张表只描述一个实体 | 把设备信息和策略规则混在一张表里 |
| 适度冗余 | 常用字段可以冗余存储,减少关联查询 | 每次查设备都要关联租户表才能拿到租户名 |
| 索引先行 | 查询条件字段提前建索引 | 百万级设备表,按状态查询没有索引 |
| 软删除 | 用状态字段标记删除,不真删数据 | 直接 DELETE,数据恢复不了 |
嗯,模型设计就聊到这里。这些代码你直接拿去用,但记得根据你的业务场景调整字段。比如你的设备需要存储地理位置,那就加个 location 字段;需要支持固件升级,就加个 firmware_url。模型是活的,别把它做死了。