ORM全称是object/rational mapping,简单讲就是建立面向对象语言(OOP)和结构化查询语言(SQL)之间的关系。
这种关系其实并不是双向的。一般指的是使用面向对象语言来描述SQL,而不是使用SQL来支持面向对象语言。
这种情况主要是有两个原因导致的:
OOP的结构要比SQL复杂的多,使用简单语言描述复杂语言本身就比较困难;
ORM主要是方便开发人员构思软件体系结构和简化具体编码而发明的。实际项目中,大量的工作也集中在使用面向对象语言实现软件功能这部分。
所以,实际上的ORM框架,基本指的都是通过框架将OOP自动翻译成SQL并执行,开发人员的工作界面保持在面向对象语言上。
核心问题
ORM框架的核心问题其实也很简单,用一句话来概括就是:如何保证弱表现力的SQL能够支持OOP的全部特性?
典型场景比如:面向对象语言具有继承和多态的特性。可以使用面向对象语言定义父类,并通过子类实现这些接口,产生新的对象。
但SQL不行,SQL底层是关系型数据库。数据库表之间是不存在谁继承谁的概念的。数据库表之间的关系只有一种,即通过使用外键,让两张表之间产生关联关系。
我们在面向对象语言中经常见到比如父类是Animal,子类是Dog的这种设计和实现。但是我们在数据库中从来没有见过"父表"是Animal,而"子表"是Dog的这种场景。其实数据库中根本就不存在父表或子表之说……
Hibernate
若谈起ORM,则Hibernate是必然会被提及的框架。Hibernate的核心功能便是对应用封装数据库操作,让访问数据库的操作尽可能像操作本地对象一样简单。
JPA的全称叫Java Persistence API,是Java中定义的关于数据持久化的一套规范。
可以简单地认为Hibernate是JPA的一种实现。当然,Hibernate除实现了JPA规范定义的功能之外,还包含了很多其特有的特性。
ORM框架的核心抽象叫EntityManager
,它用于处理所有对Entity
对象的操作。
Entity
是什么呢?Entity
其实是沟通面向对象语言和结构化语言的桥梁。Entity
本身是一个普通的Java对象。所以Java中所有操作对象的功能,在Entity
上都能使用。同时,Entity
上又定义了具体的SQL操作,对Entity
的操作,最终会通过EntityManager
转换成对数据库表的操作。
下面将基于Hibernate创建一个简单的ORM应用,初步体验ORM框架的魅力。
创建数据库
可以使用任意关系型数据库,本文中使用了MySQL的开源版本MariaDB。
在数据库中创建名为hibernate
的schema:
CREATE SCHEMA `hibernate` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ;
创建应用
使用Maven工程,创建一个名为OrmTest
的Maven工程,修改pom.xml文件,调整下Java编译器的版本为1.8:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<groupId>org.examplegroupId>
<artifactId>OrmTestartifactId>
<version>1.0-SNAPSHOTversion>
<properties>
<java.version>1.8java.version>
<maven.compiler.source>${java.version}maven.compiler.source>
<maven.compiler.target>${java.version}maven.compiler.target>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding>
properties>
project>
引入依赖
修改pom文件,增加依赖包:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<groupId>org.examplegroupId>
<artifactId>OrmTestartifactId>
<version>1.0-SNAPSHOTversion>
<properties>
<java.version>1.8java.version>
<maven.compiler.source>${java.version}maven.compiler.source>
<maven.compiler.target>${java.version}maven.compiler.target>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding>
properties>
<dependencies>
<dependency>
<groupId>org.hibernategroupId>
<artifactId>hibernate-entitymanagerartifactId>
<version>5.4.22.Finalversion>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>8.0.21version>
dependency>
<dependency>
<groupId>org.apache.logging.log4jgroupId>
<artifactId>log4j-coreartifactId>
<version>2.13.3version>
dependency>
<dependency>
<groupId>org.junit.jupitergroupId>
<artifactId>junit-jupiter-apiartifactId>
<version>5.7.0version>
<scope>testscope>
dependency>
dependencies>
project>
一共引入了四个依赖:
hibernate-entitymanager
:Hibernate工程ORM功能的核心包;mysql-connector-java
:是MySQL的JDBC驱动,用于连接数据库;log4j-core
:主要目的是将Hibernate执行的SQL语句打印出来,便于调试;junit-jupiter-api
:Junit 5,用于编写测试用例;
配置log4j
在src/main/resources下面增加log4j2.xml文件,该文件是log4j的配置文件。在文件中增加如下配置,这样就能将Hibernate的相关日志打印在控台上了:
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" />
Console>
Appenders>
<Loggers>
<Logger name="org.hibernate" level="info" additivity="false">
<AppenderRef ref="Console" />
Logger>
<Logger name="org.hibernate.SQL" level="debug" additivity="false">
<AppenderRef ref="Console" />
Logger>
<Logger name="org.hibernate.type.descriptor.sql.BasicBinder" level="trace" additivity="false">
<AppenderRef ref="Console" />
Logger>
<Logger name="org.hibernate.type.descriptor.sql.BasicExtractor" level="trace" additivity="false">
<AppenderRef ref="Console" />
Logger>
<Root level="info">
<AppenderRef ref="Console" />
Root>
Loggers>
Configuration>
创建Entity
创建org.example.hibernate.model
package,在该package下创建Message
类,代码如下:
package org.example.hibernate.model;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@Entity
public class Message {
@Id @GeneratedValue
private Long id;
private String text;
public String getText() {
return this.text;
}
public void setText(String text) {
this.text = text;
}
}
@Entity
表示这是一个Entity
对象,它告诉Hibernate需要在数据库中创建一个名为MESSAGE的表。@Id
和@GeneratedValue
是用于标注主键的,它告诉Hibernate需要在MESSAGE表中增加一个名为ID的列,且这个列是数据库的主键。
启用Hibernate
创建org.example.hibernate.config
package,在该package下创建一个SessionFactorySingleton
类。这个类我们准备用单例实现它,它维护一个静态的SessionFactory
对象。
package org.example.hibernate.config;
import org.hibernate.SessionFactory;
import org.hibernate.boot.Metadata;
import org.hibernate.boot.MetadataBuilder;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.hibernate.service.ServiceRegistry;
public class SessionFactorySingleton {
private static SessionFactory INSTANCE = null;
public static SessionFactory getInstance() {
if (INSTANCE == null) {
synchronized (SessionFactorySingleton.class) {
if (INSTANCE == null) {
init();
}
}
}
return INSTANCE;
}
private static void init() {
StandardServiceRegistryBuilder serviceRegistryBuilder = new StandardServiceRegistryBuilder();
serviceRegistryBuilder
.applySetting("hibernate.connection.driver_class", "com.mysql.cj.jdbc.Driver")
.applySetting("hibernate.connection.url", "jdbc:mysql://localhost:3306/hibernate?characterEncoding=utf8&useSSL=false")
.applySetting("hibernate.connection.username", "root")
.applySetting("hibernate.connection.password", "root1234")
.applySetting("hibernate.format_sql", "true")
.applySetting("hibernate.use_sql_comments", "true")
.applySetting("hibernate.hbm2ddl.auto", "create-drop");
ServiceRegistry serviceRegistry = serviceRegistryBuilder.build();
MetadataSources metadataSources = new MetadataSources(serviceRegistry);
metadataSources.addAnnotatedClass(org.example.hibernate.model.Message.class);
MetadataBuilder metadataBuilder = metadataSources.getMetadataBuilder();
Metadata metadata = metadataBuilder.build();
INSTANCE = metadata.buildSessionFactory();
}
}
这段代码的核心逻辑在init()
方法中,主要是初始化Hibernate相关参数,并构造一个SessionFactory
对象。
测试代码
在src/test/java目录下,创建org.example.hibernate.config
package。在该package下创建SessionFactorySingletonTest
测试类:
package org.example.hibernate.config;
import org.example.hibernate.model.Message;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
class SessionFactorySingletonTest {
@Test
public void testHibernate() {
SessionFactory sessionFactory = SessionFactorySingleton.getInstance();
Session session = sessionFactory.openSession();
session.beginTransaction();
Message message = new Message();
message.setText("This is the text stored in DB");
session.persist(message);
session.getTransaction().commit();
session.close();
session = sessionFactory.openSession();
session.beginTransaction();
List messages = session.createCriteria(Message.class).list();
assertEquals(messages.size(), 1);
assertEquals(messages.get(0).getText(), "This is the text stored in DB");
session.getTransaction().commit();
session.close();
}
}
这个测试类首先向数据库中提交了一个Message
对象。然后它在另一个Session
中查询了该对象。
测试
完整的工程结构如下图所示:
运行程序,在控制台上会输出如下信息:
/Library/Java/JavaVirtualMachines/jdk-14.0.2.jdk/Contents/Home/bin/java -ea -Didea.test.cyclic.buffer.size=1048576 -javaagent:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar=60246:/Applications/IntelliJ IDEA.app/Contents/bin -Dfile.encoding=UTF-8 -classpath /Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar:/Users/future/.m2/repository/org/junit/platform/junit-platform-launcher/1.7.0/junit-platform-launcher-1.7.0.jar:/Users/future/.m2/repository/org/junit/platform/junit-platform-engine/1.7.0/junit-platform-engine-1.7.0.jar:/Users/future/.m2/repository/org/junit/jupiter/junit-jupiter-engine/5.7.0/junit-jupiter-engine-5.7.0.jar:/Applications/IntelliJ IDEA.app/Contents/plugins/junit/lib/junit5-rt.jar:/Applications/IntelliJ IDEA.app/Contents/plugins/junit/lib/junit-rt.jar:/Volumes/data/idea/OrmTest/target/test-classes:/Volumes/data/idea/OrmTest/target/classes:/Users/future/.m2/repository/org/hibernate/hibernate-entitymanager/5.4.22.Final/hibernate-entitymanager-5.4.22.Final.jar:/Users/future/.m2/repository/org/jboss/logging/jboss-logging/3.3.2.Final/jboss-logging-3.3.2.Final.jar:/Users/future/.m2/repository/org/hibernate/hibernate-core/5.4.22.Final/hibernate-core-5.4.22.Final.jar:/Users/future/.m2/repository/org/javassist/javassist/3.24.0-GA/javassist-3.24.0-GA.jar:/Users/future/.m2/repository/antlr/antlr/2.7.7/antlr-2.7.7.jar:/Users/future/.m2/repository/org/jboss/jandex/2.1.3.Final/jandex-2.1.3.Final.jar:/Users/future/.m2/repository/com/fasterxml/classmate/1.5.1/classmate-1.5.1.jar:/Users/future/.m2/repository/javax/activation/javax.activation-api/1.2.0/javax.activation-api-1.2.0.jar:/Users/future/.m2/repository/javax/xml/bind/jaxb-api/2.3.1/jaxb-api-2.3.1.jar:/Users/future/.m2/repository/org/glassfish/jaxb/jaxb-runtime/2.3.1/jaxb-runtime-2.3.1.jar:/Users/future/.m2/repository/org/glassfish/jaxb/txw2/2.3.1/txw2-2.3.1.jar:/Users/future/.m2/repository/com/sun/istack/istack-commons-runtime/3.0.7/istack-commons-runtime-3.0.7.jar:/Users/future/.m2/repository/org/jvnet/staxex/stax-ex/1.8/stax-ex-1.8.jar:/Users/future/.m2/repository/com/sun/xml/fastinfoset/FastInfoset/1.2.15/FastInfoset-1.2.15.jar:/Users/future/.m2/repository/org/dom4j/dom4j/2.1.3/dom4j-2.1.3.jar:/Users/future/.m2/repository/org/hibernate/common/hibernate-commons-annotations/5.1.0.Final/hibernate-commons-annotations-5.1.0.Final.jar:/Users/future/.m2/repository/javax/persistence/javax.persistence-api/2.2/javax.persistence-api-2.2.jar:/Users/future/.m2/repository/net/bytebuddy/byte-buddy/1.10.10/byte-buddy-1.10.10.jar:/Users/future/.m2/repository/org/jboss/spec/javax/transaction/jboss-transaction-api_1.2_spec/1.1.1.Final/jboss-transaction-api_1.2_spec-1.1.1.Final.jar:/Users/future/.m2/repository/mysql/mysql-connector-java/8.0.21/mysql-connector-java-8.0.21.jar:/Users/future/.m2/repository/com/google/protobuf/protobuf-java/3.11.4/protobuf-java-3.11.4.jar:/Users/future/.m2/repository/org/apache/logging/log4j/log4j-core/2.13.3/log4j-core-2.13.3.jar:/Users/future/.m2/repository/org/apache/logging/log4j/log4j-api/2.13.3/log4j-api-2.13.3.jar:/Users/future/.m2/repository/org/junit/jupiter/junit-jupiter-api/5.7.0/junit-jupiter-api-5.7.0.jar:/Users/future/.m2/repository/org/apiguardian/apiguardian-api/1.1.0/apiguardian-api-1.1.0.jar:/Users/future/.m2/repository/org/opentest4j/opentest4j/1.2.0/opentest4j-1.2.0.jar:/Users/future/.m2/repository/org/junit/platform/junit-platform-commons/1.7.0/junit-platform-commons-1.7.0.jar com.intellij.rt.junit.JUnitStarter -ideVersion5 -junit5 org.example.hibernate.config.SessionFactorySingletonTest,testHibernate
17:12:30.519 [main] INFO org.hibernate.Version - HHH000412: Hibernate ORM core version 5.4.22.Final
17:12:30.784 [main] INFO org.hibernate.annotations.common.Version - HCANN000001: Hibernate Commons Annotations {5.1.0.Final}
17:12:30.974 [main] WARN org.hibernate.orm.connections.pooling - HHH10001002: Using Hibernate built-in connection pool (not for production use!)
17:12:30.984 [main] INFO org.hibernate.orm.connections.pooling - HHH10001005: using driver [com.mysql.cj.jdbc.Driver] at URL [jdbc:mysql://localhost:3306/hibernate?characterEncoding=utf8&useSSL=false]
17:12:30.985 [main] INFO org.hibernate.orm.connections.pooling - HHH10001001: Connection properties: {password=****, user=root}
17:12:30.985 [main] INFO org.hibernate.orm.connections.pooling - HHH10001003: Autocommit mode: false
17:12:30.989 [main] INFO org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl - HHH000115: Hibernate connection pool size: 20 (min=1)
17:12:31.450 [main] INFO org.hibernate.dialect.Dialect - HHH000400: Using dialect: org.hibernate.dialect.MySQL55Dialect
17:12:32.703 [main] DEBUG org.hibernate.SQL -
drop table if exists hibernate_sequence
17:12:32.707 [main] INFO org.hibernate.orm.connections.access - HHH10001501: Connection obtained from JdbcConnectionAccess [org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator$ConnectionProviderJdbcConnectionAccess@465b38e6] for (non-JTA) DDL execution was not in auto-commit mode; the Connection 'local transaction' will be committed and the Connection will be set into auto-commit mode.
17:12:32.757 [main] DEBUG org.hibernate.SQL -
drop table if exists Message
17:12:32.773 [main] DEBUG org.hibernate.SQL -
create table hibernate_sequence (
next_val bigint
) engine=InnoDB
17:12:32.776 [main] INFO org.hibernate.orm.connections.access - HHH10001501: Connection obtained from JdbcConnectionAccess [org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator$ConnectionProviderJdbcConnectionAccess@4ace284d] for (non-JTA) DDL execution was not in auto-commit mode; the Connection 'local transaction' will be committed and the Connection will be set into auto-commit mode.
17:12:32.828 [main] DEBUG org.hibernate.SQL -
insert into hibernate_sequence values ( 1 )
17:12:32.845 [main] DEBUG org.hibernate.SQL -
create table Message (
id bigint not null,
text varchar(255),
primary key (id)
) engine=InnoDB
17:12:32.915 [main] INFO org.hibernate.engine.transaction.jta.platform.internal.JtaPlatformInitiator - HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
17:12:33.065 [main] DEBUG org.hibernate.SQL -
select
next_val as id_val
from
hibernate_sequence for update
17:12:33.084 [main] DEBUG org.hibernate.SQL -
update
hibernate_sequence
set
next_val= ?
where
next_val=?
17:12:33.125 [main] DEBUG org.hibernate.SQL -
/* insert org.example.hibernate.model.Message
*/ insert
into
Message
(text, id)
values
(?, ?)
17:12:33.129 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [1] as [VARCHAR] - [This is the text stored in DB]
17:12:33.130 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [2] as [BIGINT] - [1]
17:12:33.164 [main] WARN org.hibernate.orm.deprecation - HHH90000022: Hibernate's legacy org.hibernate.Criteria API is deprecated; use the JPA javax.persistence.criteria.CriteriaQuery instead
17:12:33.183 [main] DEBUG org.hibernate.SQL -
/* criteria query */ select
this_.id as id1_0_0_,
this_.text as text2_0_0_
from
Message this_
17:12:33.189 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - extracted value ([id1_0_0_] : [BIGINT]) - [1]
17:12:33.193 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - extracted value ([text2_0_0_] : [VARCHAR]) - [This is the text stored in DB]
Process finished with exit code 0
MariaDB
在数据库中,可以看到新创建了一个名为MESSAGE的表:
查看MESSAGE表数据,可以看到测试用例插入的那条记录: