模型定义与映射:数据表与类的桥梁
说实话,ORM 最核心的价值,就是帮我们把数据库里的表,映射成代码里的类。你想想看,如果没有这层映射,我们得写多少 SQL?我早期做项目的时候,一个简单的用户查询,就得拼接字符串,还得小心 SQL 注入,那叫一个痛苦。
今天我们就来聊聊,这个映射到底是怎么玩的。我会结合 Python 的 SQLAlchemy 和 Java 的 Hibernate,把三种核心映射讲透。
一、数据表与类的映射:从二维表到对象
说白了,就是把数据库里的一行记录,变成一个对象。表名对应类名,字段对应属性。嗯,这里要注意,命名风格上有个小坑。
我个人习惯,数据库里用下划线命名法,比如 user_name。但在 Python 或 Java 类里,我更喜欢驼峰命名,比如 userName。ORM 框架一般都会提供映射策略,帮你自动转换。
核心原则:每个持久化类,对应一张表。每个实例,对应一行记录。
来看个 Python 的例子:
from sqlalchemy import Column, Integer, String, create_engine
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class User(Base):
__tablename__ = 'user_info' # 表名
id = Column(Integer, primary_key=True)
user_name = Column(String(50)) # 字段映射
email = Column(String(100))
Java 的 Hibernate 写法也类似:
@Entity
@Table(name = "user_info")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "user_name", length = 50)
private String userName;
@Column(name = "email", length = 100)
private String email;
}
你看,结构几乎一样。这就是 ORM 的魅力——屏蔽了数据库的差异。
二、字段类型映射:别让类型坑了你
类型映射这块,我踩过不少坑。数据库里的 VARCHAR 对应 Java 的 String,这没问题。但 DATETIME 呢?Python 里是 datetime,Java 里是 java.util.Date 或 LocalDateTime。
我曾经遇到过一个线上问题:数据库存的是 TIMESTAMP,Java 代码用 java.sql.Date 接收,结果时区全乱了。从那以后,我统一用 LocalDateTime,再也没出过问题。
| 数据库类型 | Python (SQLAlchemy) | Java (Hibernate) |
|---|---|---|
| INTEGER | Integer | Integer / Long |
| VARCHAR | String | String |
| DATETIME | DateTime | LocalDateTime |
| DECIMAL | Numeric / Float | BigDecimal |
| BOOLEAN | Boolean | Boolean |
| TEXT | Text | String (CLOB) |
避坑指南:我曾经用 Float 映射数据库的 DECIMAL(10,2),结果精度丢失,对不上账。后来改用 BigDecimal / Numeric,才彻底解决。金额字段,一定要用高精度类型。
三、表关系映射:一对一、一对多、多对多
这才是 ORM 的精髓。表之间的关系,在代码里怎么表达?我一个个说。
1. 一对一关系
比如用户和身份证信息。一个用户只有一个身份证,反过来也一样。
Python 写法:
class User(Base):
__tablename__ = 'user'
id = Column(Integer, primary_key=True)
profile = relationship("Profile", uselist=False, back_populates="user")
class Profile(Base):
__tablename__ = 'profile'
id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey('user.id'))
user = relationship("User", back_populates="profile")
Java 写法:
@Entity
public class User {
@Id
private Long id;
@OneToOne(mappedBy = "user")
private Profile profile;
}
@Entity
public class Profile {
@Id
private Long id;
@OneToOne
@JoinColumn(name = "user_id")
private User user;
}
注意那个 uselist=False,告诉 ORM 这不是一个列表,而是一个单独的对象。我刚开始学的时候,经常忘记这个参数,结果查出来是个列表,还得手动取第一个元素。
2. 一对多关系
这个最常见。一个用户有多篇文章,文章属于一个用户。
Python 里用 ForeignKey 加 relationship:
class User(Base):
__tablename__ = 'user'
id = Column(Integer, primary_key=True)
articles = relationship("Article", back_populates="author")
class Article(Base):
__tablename__ = 'article'
id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey('user.id'))
author = relationship("User", back_populates="articles")
Java 里用 @OneToMany 和 @ManyToOne:
@Entity
public class User {
@Id
private Long id;
@OneToMany(mappedBy = "author")
private List<Article> articles;
}
@Entity
public class Article {
@Id
private Long id;
@ManyToOne
@JoinColumn(name = "user_id")
private User author;
}
注意:一对多关系里,外键永远放在「多」的那一端。也就是 Article 表里存 user_id。我曾经见过有人把外键放在 User 表里,结果查询效率极低,还容易产生数据冗余。
3. 多对多关系
比如学生和课程。一个学生可以选多门课,一门课也可以被多个学生选。
这需要一张中间表。Python 里用 Table 定义中间表:
student_course = Table(
'student_course', Base.metadata,
Column('student_id', Integer, ForeignKey('student.id')),
Column('course_id', Integer, ForeignKey('course.id'))
)
class Student(Base):
__tablename__ = 'student'
id = Column(Integer, primary_key=True)
courses = relationship("Course", secondary=student_course, back_populates="students")
class Course(Base):
__tablename__ = 'course'
id = Column(Integer, primary_key=True)
students = relationship("Student", secondary=student_course, back_populates="courses")
Java 里用 @ManyToMany:
@Entity
public class Student {
@Id
private Long id;
@ManyToMany
@JoinTable(
name = "student_course",
joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id")
)
private List<Course> courses;
}
@Entity
public class Course {
@Id
private Long id;
@ManyToMany(mappedBy = "courses")
private List<Student> students;
}
这里有个细节:secondary 参数指定了中间表。我建议中间表里除了两个外键,最好再加一个自增主键。为什么?因为有些 ORM 框架对复合主键支持不太好,而且以后如果要扩展中间表(比如加个选课时间字段),有主键会方便很多。
四、映射的最佳实践
聊了这么多,我总结几条经验:
- 延迟加载是双刃剑:默认情况下,关联对象不会立即加载。这在列表查询时能省不少性能。但如果你在事务外访问关联对象,就会报错。我建议在需要的地方,用
joinedload或fetch = FetchType.EAGER显式指定。 - 双向关系要维护两端:比如添加文章时,不仅要设置
article.author = user,还要user.articles.append(article)。否则缓存里数据会不一致。我吃过这个亏,查了半天才发现是缓存问题。 - 命名规范要统一:我习惯在数据库里用
user_id,在代码里用user(对象引用)或userId(单纯的外键值)。别混用,否则团队协作时容易搞混。
个人小技巧:在定义关系时,我通常会先画 ER 图,理清表之间的关系。然后再写代码。这样能避免很多逻辑错误。你想想看,如果关系都搞错了,代码写得再漂亮也没用。
好了,模型定义与映射这块,核心内容就这些。下一章我们会聊到查询优化,到时候会用到今天讲的关系映射。记得把基础打牢,后面才能飞得起来。