模型定义与映射:数据表与类的桥梁

说实话,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.DateLocalDateTime

我曾经遇到过一个线上问题:数据库存的是 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 里用 ForeignKeyrelationship

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 框架对复合主键支持不太好,而且以后如果要扩展中间表(比如加个选课时间字段),有主键会方便很多。

四、映射的最佳实践

聊了这么多,我总结几条经验:

  1. 延迟加载是双刃剑:默认情况下,关联对象不会立即加载。这在列表查询时能省不少性能。但如果你在事务外访问关联对象,就会报错。我建议在需要的地方,用 joinedloadfetch = FetchType.EAGER 显式指定。
  2. 双向关系要维护两端:比如添加文章时,不仅要设置 article.author = user,还要 user.articles.append(article)。否则缓存里数据会不一致。我吃过这个亏,查了半天才发现是缓存问题。
  3. 命名规范要统一:我习惯在数据库里用 user_id,在代码里用 user(对象引用)或 userId(单纯的外键值)。别混用,否则团队协作时容易搞混。

个人小技巧:在定义关系时,我通常会先画 ER 图,理清表之间的关系。然后再写代码。这样能避免很多逻辑错误。你想想看,如果关系都搞错了,代码写得再漂亮也没用。

好了,模型定义与映射这块,核心内容就这些。下一章我们会聊到查询优化,到时候会用到今天讲的关系映射。记得把基础打牢,后面才能飞得起来。