hibernate 映射关系
关系型数据库中,多表之间存在三种关联关系,分别为一对一,一对多和多对多。
主外键的添加:
- 一对一:在任意一方引入对方主键作为外键。
- 一对多:在“多”的一方,添加“一”的一方的主键作为外键。
- 多对多:产生中间关系表,引入两张表的主键作为外键,两个主键成为联合主键。
一对一的关系就是在本类中定义对方类型的对象,如 A 类中定义 B 类类型的属性 b,B 类中定义 A 类类型的属性 a;一对多的关系,在一个 A 类类型对应多个 B 类类型的情况下,需要在 A 类以 Set 集合的方式引入 B 类类型的对象,在 B 类中定义 A 类类型的属性 a;多对多的关系,在 A 类中定义 B 类类型的 Set 集合,在 B 类中定义 A 类类型的 Set 集合,这里用 Set 集合的目的是避免数据的重复。
一对一关系映射
场景:如我们的身份证,一个人只有一个身份证,而一个身份证也就只对应一个人。还有其他的证件也是如此。
主键关联:
即让两个对象具有相同的主键值,以表明它们之间的一一对应的关系;数据库表不会有额外的字段来维护它们之间的关系,仅通过表的主键来关联。(可以自动级联)
环境搭建:
pom.xml
<dependencies> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.20</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.11</version> </dependency> <dependency> <groupId>c3p0</groupId> <artifactId>c3p0</artifactId> <version>0.9.1.2</version> </dependency> <!-- 添加Hibernate依赖 --> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-annotations</artifactId> <version>3.2.1.ga</version> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-entitymanager</artifactId> <version>3.2.1.ga</version> </dependency> <!-- SLF4J--> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.32</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-nop</artifactId> <version>1.7.2</version> </dependency> <!-- 添加javassist --> <dependency> <groupId>javassist</groupId> <artifactId>javassist</artifactId> <version>3.12.0.GA</version> </dependency> <!-- mysql数据库的驱动包 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.6</version> </dependency> </dependencies> <build> <!--配置Maven 对resource文件 过滤 --> <resources> <resource> <directory>src/main/resources</directory> <includes> <include>**/*.properties</include> <include>**/*.xml</include> <include>**/*.xls</include> <include>**/*.xlsx</include> </includes> <filtering>true</filtering> </resource> <resource> <directory>src/main/java</directory> <includes> <include>**/*.properties</include> <include>**/*.xml</include> </includes> <filtering>true</filtering> </resource> </resources> </build>
log4j.xml
### direct log messages to stdout ### log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.Target=System.out log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n ### direct messages to file hibernate.log ### #log4j.appender.file=org.apache.log4j.FileAppender #log4j.appender.file.File=hibernate.log #log4j.appender.file.layout=org.apache.log4j.PatternLayout #log4j.appender.file.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n ### set log levels - for more verbose logging change 'info' to 'debug' ### log4j.rootLogger=warn, stdout log4j.logger.org.hibernate=info #log4j.logger.org.hibernate=debug ### log HQL query parser activity #log4j.logger.org.hibernate.hql.ast.AST=debug ### log just the SQL #log4j.logger.org.hibernate.SQL=debug ### log JDBC bind parameters ### log4j.logger.org.hibernate.type=info #log4j.logger.org.hibernate.type=debug ### log schema export/update ### log4j.logger.org.hibernate.tool.hbm2ddl=debug ### log HQL parse trees #log4j.logger.org.hibernate.hql=debug ### log cache activity ### #log4j.logger.org.hibernate.cache=debug ### log transaction activity #log4j.logger.org.hibernate.transaction=debug ### log JDBC resource acquisition #log4j.logger.org.hibernate.jdbc=debug ### enable the following line if you want to track down connection ### ### leakages when using DriverManagerConnectionProvider ### #log4j.logger.org.hibernate.connection.DriverManagerConnectionProvider=trace
工具类:
package com.hibernate; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.hibernate.cfg.Configuration; public class HibernateUtils { private static SessionFactory factory; static { try { Configuration cfg = new Configuration().configure(); factory = cfg.buildSessionFactory(); }catch(Exception e) { e.printStackTrace(); } } public static SessionFactory getSessionFactory() { return factory; } public static Session getSession() { return factory.openSession(); } public static void closeSession(Session session) { if (session != null) { if (session.isOpen()) { session.close(); } } } }
实体类:
the code of IdCard.java
package com.hibernate; public class IdCard { private int id; private String cardNo; public String getCardNo() { return cardNo; } public void setCardNo(String cardNo) { this.cardNo = cardNo; } public int getId() { return id; } public void setId(int id) { this.id = id; } }
the code of Person.java
package com.hibernate; public class Person { private int id; private String name; private IdCard idCard; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public IdCard getIdCard() { return idCard; } public void setIdCard(IdCard idCard) { this.idCard = idCard; } }
hibernate.cfg.xml
<!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate Configuration DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd"> <hibernate-configuration> <session-factory> <property name="hibernate.connection.url">jdbc:mysql://localhost/hibernatelearn</property> <property name="hibernate.connection.driver_class">com.mysql.jdbc.Driver</property> <property name="hibernate.connection.username">root</property> <property name="hibernate.connection.password">root</property> <property name="hibernate.dialect">org.hibernate.dialect.MySQLDialect</property> <property name="hibernate.show_sql">true</property> <mapping resource="Person.hbm.xml"/> <mapping resource="IdCard.hbm.xml"/> </session-factory> </hibernate-configuration>
测试:
@Test public void test(){ //读取hibernate.cfg.xml文件 Configuration cfg=new Configuration().configure(); SchemaExport export=new SchemaExport(cfg); export.create(true, true); }
IdCard.hbm.xml
<?xml version="1.0"?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> <hibernate-mapping> <class name="com.hibernate.IdCard" table="t_idcard"> <id name="id"> <generator class="native"/> </id> <property name="cardNo"/> </class> </hibernate-mapping>
Person.hbm.xml
<?xml version="1.0"?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> <hibernate-mapping> <class name="com.hibernate.Person" table="t_person"> <id name="id"> <generator class="foreign" > <param name="property">idCard</param> </generator> </id> <property name="name"/> <one-to-one name="idCard" class="com.hibernate.IdCard" constrained="true"/> </class> </hibernate-mapping>
在上面的代码中我们需要特别注意的就是主键生成机制,以及one-to-one标签里的代码。
在one-to-one节点中对一对一关系进行申明。上面的配置中:
- 通过one-to-one节点,我们将User类与IdCard类相关联
- constrained(约束),只能在one-to-one的映射中使用,一般在主表映射中,有外键的那个表,如果constrained=true,则表明存在外键与关联表对应,并且关联表中肯定存在对应的键与其对应,另外,该选项最关键的是影响save和deleted先后顺序,如果是增加的时候,如果constrained=true则会增加关联表,然后增加本表,删除的时候是先删除本表然后删除关联的表。
- one-to-one的单向关联中,如果constrained=false则会在查询的时候全部取出来,用left outer join的方式 如果constrained=true,hibernate会延迟加载sql,只会把主表查询出来,等有用到关联表的时候再发出sql语句去取出来
- 使主键生成器与外键共享主键的值。也就是说两张表的id是一样的。
测试:
@Test public void testSave1(){ Session session = null; try { session = HibernateUtils.getSession(); session.beginTransaction(); IdCard idCard=new IdCard(); idCard.setCardNo("8888888888"); // session.save(idCard); Person person=new Person(); person.setName("zs"); person.setIdCard(idCard); session.save(person); Person person1=new Person(); person1.setName("lis1"); person1.setIdCard(idCard); //一对一主键关联影射中,默认了cascade属性 所以不会出现TransientObjectException异常 // session.save(person1); session.getTransaction().commit(); }catch(Exception e) { e.printStackTrace(); session.getTransaction().rollback(); }finally { HibernateUtils.closeSession(session); } }
上面的测试代码中需要特别注意没有对idCard进行存储。那么会发什么呢。这里我们session.save(idCard);打个断点,发现执行完save语句后。hibernate发送了一条insert语句。
也就是说在此处hibernate想数据库添加了一条数据。(constrained=true则会增加关联表,然后增加本表。)接着事件提交后hibernate就会想数据库中插入本表的数据。
查询:
public void testLoad1(){ Session session = null; try { session = HibernateUtils.getSession(); session.beginTransaction(); Person person=(Person)session.load(Person.class,21); System.out.println ("person.name="+person.getName()); System.out.println ("idCard.cardNo="+person.getIdCard().getCardNo()); session.getTransaction().commit(); }catch(Exception e) { e.printStackTrace(); session.getTransaction().rollback(); }finally { HibernateUtils.closeSession(session); } }
唯一外键关联:
外键关联,本来是用于多对一的配置,但是加上唯一约束条件之后(采用标签来映射,指定多的一端unique为true,这样就限制了多的一端的多重性为一),也可以用来表示一对一关联关系,其实它就是多对一的特殊情况。
User.hbm.xml修改如下
<?xml version="1.0"?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> <hibernate-mapping> <class name="com.bjsxt.hibernate.Person" table="t_person"> <id name="id"> <generator class="native"/> </id> <property name="name"/> <many-to-one name="idCard" unique="true"/> </class> </hibernate-mapping>
unique=“true” 唯一约束,这样就同样做到了one-to-one的效果。
我们发现上面的都是单向的一对一关系,也就是说可以通过user来查找到IDcard,但是反过来就不行了。
双向一对一关联映射:
双向一对一主键映射关键映射代码——在IdCard端新加入如下标签映射:
<one-to-one name="person"/>
双向一对一唯一外键映射关键映射代码——在IdCard端新加入如下标签映射:
<one-to-one name="person" property-ref="idCard"/>
注意:一对一唯一外键关联双向采用标签映射,必须指定标签中的property-ref属性为关系字段的名称
一对多的映射关系
单向
一对多关联映射在系统实现中非常常见。例如我们的学生和老师的关系,如一个老师有多个学生。这样在系统的设计中就反映出一对多的关联。
注意:它与多对一的区别是维护的关系不同
*多对一维护的关系是:多指向一的关系,有了此关系,加载多的时候可以将一加载上来
*一对多维护的关系是:一指向多的关系,有了此关系,在加载一的时候可以将多加载上来
关键代码:
@Getter @Setter public class Classes { private int id; private String name; private Set students; }
@Setter @Getter public class Student { private int id; private String name; }
<hibernate-mapping package="com.bjsxt.hibernate"> <class name="Classes" table="t_classes"> <id name="id"> <generator class="native"/> </id> <property name="name"/> <set name="students"> <key column="classesid" /> <one-to-many class="Student"/> </set> </class> </hibernate-mapping>
<hibernate-mapping> <class name="com.bjsxt.hibernate.Student" table="t_student"> <id name="id"> <generator class="native"/> </id> <property name="name"/> </class> </hibernate-mapping>
测试:
public void testSave1() { Session session = null; try { session = HibernateUtils.getSession(); session.beginTransaction(); Student student1 = new Student(); student1.setName("zhangsan"); session.save(student1); Student student2 = new Student(); student2.setName("lisi"); session.save(student2); Set students = new HashSet(); students.add(student1); students.add(student2); Classes classes = new Classes(); classes.setName("zte"); classes.setStudents(students); session.save(classes); session.getTransaction().commit(); }catch(Exception e) { e.printStackTrace(); session.getTransaction().rollback(); }finally { HibernateUtils.closeSession(session); } }
截图:
这里我们可以看到,hibernate是先创建两张表然后再对表t_students添加外键
上面的运行中我们看到每执行一条save控制台就输出一条select语句,把对象所对应的数据存入数据库。但此时出了少的一方(t_class表)中的id是确定的其他的两条数据中打的id是null.
提交事务后再对,student表中的id做修改,也就是说在数据没有提交时id数据是为null的。一提交事务在对表中的id进行修改。我们发现update语句只有对student表的修改,也就是说变class表中的id是已经确定的。使两张表产生关联。
这里关系的维持是在少的一方,在添加主外键的时候hibernate更具set标签在生成。classes.hb.xml中name对应Classes类中students属性,key对应表中的域(classesid),one-to-many告诉hibernate要和那张表关联。当事务提交时,hibernate属性students所对应域(外键classid)来找到t_classes表中的主键所对应的值。再把t_students表中的外键值更新(update)。
双向
采用一对多双向关联映射的目的主要是为了主要是为了解决一对多单向关联的缺陷而不是需求驱动的。比如,为了让老师也可以找到学生们。
一对多双向关联的映射方式:、
- 在一的一端的集合上采用标签,在多的一端加入一个外键
- 在多的一端采用标签
注意:标签和标签加入的字段保持一直,否则会产生数据混乱
关键映射代码:
@Setter @Getter public class Student { private int id; private String name; private Classes classes; }
@Setter @Getter public class Classes { private int id; private String name; private Set students; }
<hibernate-mapping> <class name="com.bjsxt.hibernate.Student" table="t_student"> <id name="id"> <generator class="native"/> </id> <property name="name"/> <many-to-one name="classes" column="classesid"/> </class> </hibernate-mapping>
<hibernate-mapping package="com.bjsxt.hibernate"> <class name="Classes" table="t_classes"> <id name="id"> <generator class="native"/> </id> <property name="name"/> <set name="students" inverse="true" cascade="all"> <key column="classesid"/> <one-to-many class="Student"/> </set> </class> </hibernate-mapping>
通过对比单向发现:在少的一方添加了private Classes classes;字段,hbm中set里添加了属性inverse和cascade。并且多的一方hbm文件中添加了。
注意:
inverse属性可以用在一对多和多对多双向关联上,inverse属性默认为false,为false表示本端可以维护关系,如果inverse为true,则本端不能维护关系,会交给另一端维护关系,本端失效。所以一对多关联映射我们通常在多的一端维护关系,让一的一端失效。
inverse是控制方向上的反转,只影响存储
hibernate创建表以及数据库中表的结构也是和之前单向是一样的。
然而对数据的存储时发现提交事务后只有三条
多对多映射关系,
hibernate关联关系中相对比较特殊的就是多对多关联,多对多关联与一对一和一对不同,他需要借助中间别来完成多对多映射关系的保存。
由于多对多关联的性能不佳(由于引入了中间表,一次读取操作需要反复数次查询),因此在设计中应避免大量使用。同时在多对多关系中,应根据情况采取延迟加载机制来避免无所谓的性能开销。
单向
上面的场景中,为了实现老师可以找到学生们,就可以用多对多的关系实现。不过还需要添加一张表就是。
@Setter @Getter public class Role { private int id; private String name; }
@Getter @Setter public class User { private int id; private String name; private Set roles; }
<hibernate-mapping> <class name="com.bjsxt.hibernate.Role" table="t_role"> <id name="id"> <generator class="native"/> </id> <property name="name"/> </class> </hibernate-mapping>
<hibernate-mapping> <class name="com.bjsxt.hibernate.User" table="t_user"> <id name="id"> <generator class="native"/> </id> <property name="name"/> <set name="roles" table="t_user_role"> <key column="userid"/> <many-to-many class="com.bjsxt.hibernate.Role" column="roleid"/> </set> </class> </hibernate-mapping>
把hbm代码代码放在一起展示发现,比之前的其他代码关系的代码似乎更加简洁。
接下来我们生成一下表,发现hibernate给我们生成了三张表,
添加数据关键代码:
Session session = null; try { session = HibernateUtils.getSession(); session.beginTransaction(); Role r1 = new Role(); r1.setName("aaaa"); session.save(r1); Role r2 = new Role(); r2.setName("bbbb"); session.save(r2); Role r3 = new Role(); r3.setName("cccc"); session.save(r3); User u1 = new User(); u1.setName("zhangsan"); Set u1Roles = new HashSet(); u1Roles.add(r1); u1Roles.add(r2); u1.setRoles(u1Roles); session.save(u1); User u2 = new User(); u2.setName("lisi"); Set u2Roles = new HashSet(); u2Roles.add(r2); u2Roles.add(r3); u2.setRoles(u2Roles); session.save(u2); User u3 = new User(); u3.setName("wangwu"); Set u3Roles = new HashSet(); u3Roles.add(r1); u3Roles.add(r2); u3Roles.add(r3); u3.setRoles(u3Roles); session.save(u3); session.getTransaction().commit();
运行截图:
查找数据:
session = HibernateUtils.getSession(); session.beginTransaction(); User user = (User)session.load(User.class, 3); System.out.println(user.getName()); for (Iterator iter=user.getRoles().iterator(); iter.hasNext();) { Role role = (Role)iter.next(); System.out.println(role.getName()); }
双向
双向的目的就是为了两端都能将对方加载上来,和单向多对多的区别就是双向需要在两端都加入标签映射,需要注意的是:
* 生成的中间表名称必须一样
* 生成的中间表中的字段必须一样
关键代码:
@Getter @Setter public class User { private int id; private String name; private Set roles; }
@Getter @Setter public class Role { private int id; private String name; private Set users; }
<hibernate-mapping> <class name="com.bjsxt.hibernate.User" table="t_user"> <id name="id"> <generator class="native"/> </id> <property name="name"/> <set name="roles" table="t_user_role"> <key column="userid"/> <many-to-many class="com.bjsxt.hibernate.Role" column="roleid"/> </set> </class> </hibernate-mapping>
<hibernate-mapping> <class name="com.bjsxt.hibernate.Role" table="t_role"> <id name="id"> <generator class="native"/> </id> <property name="name"/> <set name="users" table="t_user_role" order-by="userid"> <key column="roleid"/> <many-to-many class="com.bjsxt.hibernate.User" column="userid"/> </set> </class> </hibernate-mapping>
继承映射
1、理解如何映射
因为类继承树肯定是对应多个类,要把多个类的信息存放在一张表中,必须有某种机制来区分哪些记录是属于哪个类的。
这种机制就是,在表中添加一个字段,用这个字段的值来进行区分。用ibernate实现这种策略的时候,有如下步骤:
- 父类用普通的标签定义
- 在父类中定义一个discriminator,即指定这个区分的字段的名称和类型 如:
- 子类使用标签定义,在定义subclass的时候,需要注意如下几点:
- Subclass标签的name属性是子类的全路径名
- 在Subclass标签中,用discriminator-value属性来标明本子类的discriminator字段(用来区分不同类的字段)的值
- Subclass标签,既可以被class标签所包含(这种包含关系正是表明了类之间的继承关系),也可以与class标签平行。 当subclass标签的定义与class标签平行的时候,需要在subclass标签中,添加extends属性,里面的值是父类的全路径名称。子类的其它属性,像普通类一样,定义在subclass标签的内部。
2、理解如何存储
存储的时候hibernate会自动将鉴别字段值插入到数据库中,在加载数据的时候,hibernate能根据这个鉴别值
正确的加载对象
多态查询:在hibernate加载数据的时候能鉴别出正真的类型(instanceOf)
get支持多态查询
load只有在lazy=false,才支持多态查询
hql支持多态查询
有如下的继承关系:
hbm关键代码如下:
<hibernate-mapping package="com.bjsxt.hibernate"> <class name="Animal" table="t_animal" lazy="true"> <id name="id"> <generator class="native"/> </id> <discriminator column="type" type="string"/> <property name="name"/> <property name="sex"/> <subclass name="Pig" discriminator-value="P"> <property name="weight"/> </subclass> <subclass name="Bird" discriminator-value="B"> <property name="height"/> </subclass> </class> </hibernate-mapping>
注意:
当lazy="true"
: 代码输出的是no
Animal animal = (Animal)session.load(Animal.class, 1); //因为load默认支持lazy,因为我们看到的是Animal的代理对象 //所以通过instanceof是反应不出正真的对象类型的 //因此load在默认情况下是不支持多态查询的 if (animal instanceof Pig) { System.out.println(animal.getName()); }else { System.out.println("no"); }
当lazy="false"
: 代码输出的是实际的类型名
hibernate默认懒加载是开启的。
说明:
所谓的懒加载可以定义为:
1、延时加载,即当对象需要用到的时候再去加载。其实就是所谓的重写对象的get方法,
2、当系统或者开发者调用对象的get方法时,再去加载对象。
懒加载
什么是懒加载?
懒加载其实就是延时加载,即当对象需要用到的时候再去加载
有如下的主要代码:
@Getter @Setter public class Group { private int id; private String name; }
<hibernate-mapping> <class name="com.bjsxt.hibernate.Group" table="t_group" lazy="true"> <id name="id"> <generator class="native"/> </id> <property name="name"/> </class> </hibernate-mapping>
注意:hibernate默认懒加载是开启的。
针对于开启懒加载后,对数据库进行查询,会出现一下的情况:
//不会发出sql Group group = (Group)session.load(Group.class, 1); //不会发出sql System.out.println("group.id=" + group.getId()); //会发出sql System.out.println("group.name=" + group.getName());
并且:hibernate支持lazy策略只有在session打开状态下有效,当session关闭后,不能正确输出,抛出LazyInitializationException 异常,因为session已经关闭。