Hibernate3 第二天

Hibernate3 第二天

第一天回顾:

  1. 三个准备
  • 创建数据库
  • 准备po和hbm文件
  • 准备灵魂文件hibernate.cfg.xml
  1. 七个步骤
  • 1 加载配置文件Configuration
  • 2 创建会话工厂SessionFactory
  • 3 获取连接Session
  • 4 开启事务Transaction
  • 5 各种操作
  • 6 提交事务commit
  • 7 关闭连接close

     

     

    今天内容安排:

  1. Hibernate的持久化对象(PO)相关状态和操作。(重点理解)
  • Hibernate持久化对象的状态(3个)和转换。
  • Session的一级缓存。
  • Session一级缓存的快照(snapshot)。
  1. 多表关联映射配置和操作(重点应用)
  • 一对多关联映射配置和操作、以及级联配置、外键维护配置。
  • 多对多关联映射配置和操作。

     

    学习目标:

  1. 掌握Hibernate的核心概念:PO的状态+一级缓存和快照
  2. 掌握多表映射的配置、级联配置和增删改的操作(项目中使用)
  3. 逐步学会和习惯使用debug

 

  1. Hibernate的持久化对象相关概念和操作

    1. Hibernate持久化对象(po)的状态和转换

      1. 持久化对象的状态

官方描述:

 

Hibernate 将操作的PO对象分为三种状态:

  • 瞬时 (Transient )/ 临时: 通常new 创建对象(持久化类),未与Session关联,没有OID
  • 持久 (Persistent) : 在数据库存在对应实例拥有持久化标识OID与Session关联(受session管理)
  • 脱管 (Detached)/游离:当Session关闭后,持久状态对象与Session断开关联,称为脱管对象,此时也持有OID

 

搭建测试环境:创建项目Hibernate3_day02

  1. 导入开发jar包(11个),将hibernate.cfg.xml、log4j.properties复制到src,修改Hibernate.cfg.xml的jdbc连接参数,导入hibernateUtils工具类。
  2. 创建包:cn.itcast.a_postate,在包中创建Customer类,代码如下:

     

  3. 编写hbm映射

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE hibernate-mapping PUBLIC

"-//Hibernate/Hibernate Mapping DTD 3.0//EN"

"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">

<hibernate-mapping>

    <!-- 配置java类与数据表的对应关系

     name:java类名

         table:表名

     -->

    <class name="cn.itcast.a_postate.Customer" table="t_customer">

        <!-- 配置主键 -->

        <id name="id">

            <generator class="native"></generator>

        </id>

        <!-- 配置其他属性 -->

        <property name="name"></property>

        <property name="age"></property>

    </class>

</hibernate-mapping>

 

  1. 在hibernate.cfg.xml加载映射配置

5 创建TestPOState类,描述对象的三状态。

在类中编写测试testSave方法:代码如下:

@Test

    public void testSave(){

        //瞬时态:

        //特点:没有OID,new出来的一个对象,不与session关联,不受session管理,数据库中没有对应的记录

        Customer customer = new Customer();

        customer.setName("rose");

        customer.setAge(18);

        System.out.println(customer);

        

        Session session = HibernateUtils.openSession();

        session.beginTransaction();

        

        //在save执行之前,customer都只是瞬时态

        //当save执行之后,customer就处于持久态

        session.save(customer);

        //持久态

//        /特点:有OID,数据库存在对应的记录,与session关联,受session管理

        System.out.println(customer);

        session.getTransaction().commit();

        session.close();

        

        //只要session已关闭,那么久处于脱管态

        //有OID,数据库中存在对应的记录,但是不与session关联,不受session管理

        System.out.println(customer);

        

    }

 

 

三种状态的区别分析:

  • 持久态和瞬时态、脱管态:容易区别,持久态主要特点是与Session关联,而且数据库中有对应记录拥有OID,因此,只要与session关联的就是持久态

瞬时态和脱管态:相同点是都和Session没关联,不同点是瞬时态没有OID,而脱管态有OID。

 

【思考】

通过上述的分析,发现,瞬时态和脱管态对象就差一个OID,那么瞬时态的对象中给主键ID属性赋值后就是脱管态了么?

未必!

首先,需要区分持久化标识OID对象中主键ID属性的关系:

  • 在持久化之前,虽然有ID属性,但数据库中没有对应的数据,那么此时OID是null;
  • 在持久化之后,这个ID属性值被插入数据库中当主键了,数据库中有对应的数据了,此时OID就有值了,而且与主键值保持一致性,比如类型、长度等。

因此:OID和PO对象中主键ID属性的区别就是:数据库存在不存在,如果存在就是OID,如果不存在,那就是个ID属性而已。

 

瞬时态和脱管态的区别总结:

  • 脱管态对象:有持久化的标识oid,并且在数据库中存在。
  • 瞬时态对象:无持久化标识oid,或者有id但在数据库中不存在的。

 

例如:

Customer对象具有Id属性值,如果数据库中不存在,则该对象还是瞬时态对象,如果数据库中存在,则认为是脱管态的。

 

【三者的区别最终总结】:

对于三者:在session中存在的,就是持久化对象不存在的就是瞬时或脱管对象

对于瞬时和脱管对象:有oid(持久化标识)的就脱管对象,没有的就是瞬时对象

OID一定是与数据库主键一一对应的

 

是否有持久化标识OID

session是否存在

数据库中是否有

瞬时态对象-临时状态

n

n

n

持久态对象

y

y

y/(n:没有提交)

脱管态对象-游离

y

n

y

 

  1. 持久化对象状态的相互转换

持久化对象状态转换图(官方规范):

 

 

 

【分析】:(各状态对象的获取方式以及不同状态之间转换的方法介绍):

  • 瞬时对象:

    如何直接获得 --- new 出来

    转换到持久态 ---- save、saveOrUpdate 保存操作

    转换到脱管态 ---- setId 设置OID持久化标识(这个id是数据库中存在的)

  • 持久对象

    如何直接获得 ---- 通过session查询方法获得 get、load、createQuery、createSQLQuery、createCriteria

    转换到瞬时态 ---- delete 删除操作 (数据表不存在对应记录 )(其实还有id,只是不叫OID)

    转换到脱管态 ---- close 关闭Session, evict、clear 从Session清除对象

  • 脱管对象

    如何直接获得 ----- 无法直接获得 ,必须通过瞬时对象、持久对象转换获得

    转换到瞬时态 ---- 将id设置为 null,或者手动将数据库的对应的数据删掉或者将id修改成数据库中不存在的

    转换到持久态 ---- update、saveOrUpdate、lock (对象重新放入Session ,重新与session关联)

 

在Hibernate所有的操作只认OID,如果两个对象的OID一致,它就直接认为是同一个对象。

 

  1. Session的一级缓存(重点理解)

    1. 什么是一级缓存?

又称为:hibernate一级缓存、session缓存、session一级缓存

 

  • 缓存的概念:在内存上存储一些数据

缓存的介质是一般是内存(硬盘),用来存放数据,当第一次查询数据库的时候,将数据放入缓存,当第二次继续使用这些数据的时候,就不需要要查询数据库了,直接从缓存获取,

 

 

  • 一级缓存概念:

在Session接口中实现了一系列的java集合,这些java集合构成了Session的缓存,只要Session的生命周期没有结束,session中的数据也就不会被清空。

 

  • 缓存作用:

将数据缓存到内存或者硬盘上,访问这些数据,直接从内存或硬盘加载数据,无需到数据库查询。

好处: 快! 降低数据库压力。

 

  • 一级缓存的生命周期:

Session中对象集合(map),在Session创建时,对象集合(map)也就创建,缓存保存了Session对象数据,当Session销毁后,集合(map)销毁, 一级缓存释放 !

  • 什么对象会被放入一级缓存?

只要是持久态对象,都会保存在一级缓存 (与session关联的本质,就是将对象放入了一级缓存

 

一级缓存的作用:第一次get/load的时候,肯定会发出sql语句,查询数据库,(此时会将数据放入一级缓存),只要session不关闭,

第二次get/load的时候,直接从缓存中读取数据,不会发出sql语句,查询数据库(这里的数据指的是同一条记录:OID相等)

 

【示例】证明一级缓存的存在性!

通过多次查询同一个po对象数据,得到同一个对象,且第二次不再从数据库查询,证明一级缓存存在。

@Test

    public void testFirstCacheExist(){

        /**

         * 证明一级缓存的存在性

         * 证明思路:

         * 缓存的作用就是用来少查数据库的,提高访问速度

         * 第一步,通过get/load查询数据,由于是第一次查询,所以必然发出sql语句,查询数据库

         * 第二步,不关闭session,继续使用当前的session去get/load数据,观察是否发出sql语句

         * 如果不发出,表明是从一级缓存取出的数据

         */

        Session session = HibernateUtils.openSession();

        session.beginTransaction();

        

        //第一步:此时必然发出sql语句,然后自动将数据放入一级缓存

        Customer customer = (Customer) session.get(Customer.class, 1);

        System.out.println(customer);

        

        //第二步:此时不会发出sql语句,直接从一级缓存获取数据

        Customer customer2 = (Customer) session.get(Customer.class, 1);

        System.out.println(customer2);

        

        session.getTransaction().commit();

        session.close();

    }

 

测试(同一个对象):

  1. 一级缓存的生命周期

一级缓存的生命周期就是session的生命周期,不能跨Session,可以说,一级缓存和session共存亡

【示例】

使用两个不同Session来测试生命周期。(一级缓存和session共存亡)

@Test

    public void testFirstCachelifecycle(){

        /**

         * 一级缓存的声明周期:与session同生命共存亡

         * 如何证明一级缓存的生命周期?

         * 只要证明数据不能跨session

         * 1 获取session1,通过session1拿到customer对象,此时必然发出sql语句,关闭session1

         * 2 获取session2,通过session2继续抓取customer对象,观察第二次是否发出sql语句

         * 如果发出,,表名session1销毁的时候,把数据也销毁了

         */

        Session session1 = HibernateUtils.openSession();

        session1.beginTransaction();

        

        //此时必然发出sql语句,因为是第一次查询

        Customer customer = (Customer)session1.get(Customer.class, 1);

        

        System.out.println(customer);

        

//        此处如果需要查询Customer,会发sql语句吗?答:不会,直接走一级缓存

        //也能证明数据成功存入了一级缓存

        Customer customer2 = (Customer)session1.get(Customer.class, 1);

        System.out.println(customer2);

        

        session1.getTransaction().commit();

        session1.close();

        

        /**********第二次*********/

        Session session2 = HibernateUtils.openSession();

        session2.beginTransaction();

        

        //此时发sql语句吗?答:发,因为session1中的数据跟随session1一起销毁了

        Customer customer3 = (Customer)session2.get(Customer.class, 1);

        System.out.println(customer3);

        

        session2.getTransaction().commit();

        session2.close();

        

        

    }

 

测试:

 

小结:缓存的作用,可以提高性能,减少数据库查询的频率。

[补充:原则]所有通过hibernate操作(session操作)的对象都经过一级缓存。

一级缓存是无法关闭的!内置的!hibernate自己维护的!

 

  1. Session一级缓存的快照

    1. 什么是一级缓存的快照(snapshot)

什么是快照?

答:快照,是数据在内存中的副本,是数据库中数据在内存中的映射。

 

如:

一句话:

快照跟数据库数据保持一致

快照的作用就是用来更新数据的。

 

  1. 一级缓存快照的原理(图)

采用快照技术进行更新,不需要手动的调用update 方法,完全是自动的发出update语句。

保正一级缓存、数据库、快照的一致性

【注意】

  1. 持久态对象原则:po对象尽量保持与数据库一一致。
  2. 当一级缓存和快照不一致的时候,会先发出update语句,将一级缓存同步到数据库(发出update语句),然后当同步成功之后,再自动内部同步快照。保证三者的一致性。

 

  1. 一级缓存快照的能力

一级缓存的快照是由Hibernate来维护的,用户可以更改一级缓存(PO对象的属性值),无法手动更改快照!

快照的主要能力(作用)是:用来更新数据!当刷出缓存的时候,如果一级缓存和快照不一致,则更新数据库数据。

【示例】

通过改变查询出来的PO的属性值,来查看一级缓存的更改;通过提交事务,来使用快照更新数据。

@Test

    public void testSnapShot(){

        /**

         * 证明快照的能力:自动更新数据

         * 1 从数据库查找一个对象,改变对象的某个值,手动的flush,看控制台是否发出sql语句,

         * 如果控制台发出update语句,就可以表明快照的能力

         */

        Session session = HibernateUtils.openSession();

        session.beginTransaction();

        //get的时候,不光将数据放入一级缓存,还同时将数据同步到了快照中

        Customer customer = (Customer)session.get(Customer.class, 1);    

        

        System.out.println(customer);

        //修改customer对象的值

        customer.setName("lucy");

        

        //这个值是改变过后的值,是内存中的值

        System.out.println(customer);

        

        //提交事务

        //如果不手动flush,在事务commit的时候,会先flush,在commit

        session.getTransaction().commit();

        session.close();

        

        

    }

    

 

 

【能力扩展】快照可以用来更新数据,而且,可以用来更新部分数据

 

【问题】update也是更新数据,快照也是更新数据?两个有什么区别?

Update更新的时候,会将所有值都更新,如果有某个属性没有赋值,值将会被置空

 

快照符合我们修改的要求:先查后改

 

 

 

  1. 刷出缓存的时机

什么叫刷出缓存?

Session能够在某些时间点,按照缓存中对象的变化来执行相关的SQL语句,来同步更新数据库,这一过程被成为刷出缓存(flush)。

 

通俗的说法:将一级缓存的数据同步到数据库,就是刷出缓存

 

 

什么情况下session会执行 flush操作?

 

刷新缓存的三个时机:

  • 事务提交commit():该方法先刷出缓存(session.flush()),然后再向数据库提交事务。
  • 手动刷出flush():直接调用session.flush()。
  • 查询操作:当Query查询(get、load除外,这两个会优先从一级缓存获取数据)时,会去比较一级缓存和快照,如果数据一致,则去数据库直接获取数据,如果缓存中持久化对象的属性已经发生了变化,(一级缓存和快照不一样了),则先刷出缓存,发出update语句,然后查询,以保证查询结果能够反映出持久化对象的最新状态。(Query查询数据不走一级缓存)

 

【补充理解】:

关于Hibernate如何识别同一个对象?

根据OID,

问题:假如先查询出来一个对象oid是1001,数据库主键也是1001,但其他字段的属性不一样,那么,再次查询数据库的数据出来的对象,和原来的对象是否是一个对象?

答案:是!

 

【示例】

1 、通过commit 的方式隐式的刷出缓存(证明略)

 

2 、通过flush的方式手动的刷出缓存

//采用flush的方式手动的刷出缓存

    @Test

    public void testflushcache2()

    {

        Session session = HibernateUtils.openSession();

        // 开启 事务

        session.beginTransaction();

        

        //获取数据

        Customer customer = (Customer)session.get(Customer.class, 1);

        

        System.out.println(customer);

          

        

        customer.setName("rose");

        

        //手动的flush,发出update语句,更新数据库,并且同时更新快照

        session.flush();

        

        session.getTransaction().commit();

        //特点:在数据库中存在对应的记录,有OID,但是不受session管理

        session.close();

        

    }

 

 

 

3 、使用Query的时候,(不包含get、laod:原因:get和load的处理方式,是直接获取缓存的数据,即使一级缓存和快照的数据不一致)

会去比较一级缓存和快照是否一致,如果一致,他直接去查询(1条select语句)

当不一致了,会先发出update语句,更新数据库,然后在查询(1条update语句,1条select语句)

 

3.1 测试get和load 的处理方式

@Test

    public void testGetAndLoad_Cache(){

        /**

         * 证明get和load优先从缓存取数据,哪怕一级缓存和快照的数据不一致,

         * 它也是直接取缓存数据

         * 证明思路:

         * 第一步,将数据放入一级缓存和快照

         * 第二步,取 ,观察是否发出sql语句和数据

         * 第三步,改

         * 第四部,取 ,观察是否发出sql语句和数据

         *

         */

        Session session = HibernateUtils.openSession();

        session.beginTransaction();

        

        // 1 取,并且放入一级缓存和快照

        Customer customer1 = (Customer) session.get(Customer.class, 1);

        System.out.println(customer1);

        //2 取:肯定不发sql语句,直接从缓存取

        Customer customer2 = (Customer) session.get(Customer.class, 1);

        System.out.println(customer2);

        //3 改:

        customer2.setName("tom");

        //4 取,虽然此时一级缓存和快照不一致,但是get/load也是直接抓取缓存数据

        Customer customer4 = (Customer) session.get(Customer.class, 1);

        System.out.println(customer4);

        

        //隐式flush

        session.getTransaction().commit();

        session.close();

    }

    

 

 

3.2 测试query的工作方式:

【证明1】query对象不走一级缓存

 

    //query不走一级缓存

    @Test

    public void testQuery_cache(){

//        证明query不使用session缓存的数据,即使缓存中有,它也会发出sql语句,查询数据库

        Session session = HibernateUtils.openSession();

        session.beginTransaction();

        

//        /此处也会发出sql语句

        Customer customer = (Customer)session.get(Customer.class, 1);

        System.out.println(customer);

        

        //此时必须发出sql语句,因为它不会直接从一级缓存中拿数据

        Customer customer2 = (Customer)session.createQuery("from Customer where id = 1").uniqueResult();

        System.out.println(customer2);

          

        

        session.getTransaction().commit();

        session.close();

    }

 

【证明2】但是Query对象在查询数据的时候,会去校验一级缓存和快照的数据是否一致,

如果不一致,发出update语句,更新数据库,然后再发出sql查询语句

//证明2:query对象虽然不从一级缓存取数据,但是在它去数据库查找数据之前,会干这么一件事情:

    // 会比较一级缓存和快照是否一致,

    // 如果一致,直接去数据库查找需要的数据

    // 如果不一致,先flush(先发出update语句),再去数据库查找需要的数据

    @Test

    public void testQuery2(){

        Session session = HibernateUtils.openSession();

        session.beginTransaction();

        

        Customer customer1 = (Customer) session.get(Customer.class, 1);

        

        System.out.println(customer1);

        

        customer1.setName("tom");

        

        Customer customer2 = (Customer) session.createQuery("from Customer where id = 1").uniqueResult();

        

        System.out.println(customer2);

        

        session.getTransaction().commit();

        session.close();

    }

    

 

 

【提示】

flush和commit的区别:

  • flush是发语句的。
  • commit的是数据库层面的是否保存更改的数据(是否提交数据,是否持久化数据到数据库),若不手动发出flush, hibernate在commit之前自动先flush();

 

  1. 一级缓存的刷出模式--(了解)

 

问题:能否改变一级缓存的刷出时机?答案是可以的.

【示例】

通过在session上设置手动flush模式测试:只能通过flush刷出缓存

@Test

    public void testFlushMode(){

        Session session = HibernateUtils.openSession();

        session.beginTransaction();

        // 1 第一步证明:先不设置FlushMode,看运行结果

        Customer customer = (Customer)session.get(Customer.class, 1);

        //改变缓存中对象的属性

        customer.setName("lucy");

          

        

        // 2 第二证明:设置FlushMode,看运行结果

        //这个一旦设置,只有手动flush的时候,才会发出update语句

        session.setFlushMode(FlushMode.MANUAL);

        //第二种方式,手动flush,发出update语句

        session.flush();

        

        //在第一种情况下,此处必然发出update语句

        //在第二种情况下,此处必然不会发出update语句

        session.getTransaction().commit();

        session.close();

        

    }

 

  1. 一级缓存的常用操作

操作一级缓存中的对象

 

一级缓存除了可以flush之外,还可以清除(clear,evict)、重载(refresh)。

 

  • flush:刷出一级缓存

作用:当一级缓存发生变化时,即和快照不同时,刷出一级缓存,会自动向数据库提交update语句。

  • clear:清除一级缓存中所有的数据

作用:清除一级缓存中的所有对象,这些对象被清除后,会从持久态对象变成脱管态.

【扩展理解】

持久态对象与session关联的另一层含义就是对象在一级缓存中存在。

  • evict:清除一级缓存指定对象

作用:清除一级缓存中的指定对象,使对象变成脱管态的。

  • refresh:刷新一级缓存

    通俗的讲:不管内存中对象是否发生了更新,重新将数据库中的内容加载到缓存中,覆盖原来的值

     

 

@Test

    public void testClearAndEvict(){

        Session session = HibernateUtils.openSession();

        session.beginTransaction();

        //此时数据会被放入一级缓存

        Customer customer = (Customer)session.get(Customer.class, 1);

        System.out.println(customer);

        

        //在第二次获取之前插入代码:clear 一级缓存

        //clear:清除一级缓存中的所有数据

//        session.clear();

        //evict:清除一级缓存的指定对象,主要清除的是OID=1的这个对象

        session.evict(customer);

          

        

        //由于第一次get的时候,已经发出了sql语句查询数据库,所以,第二次get的时候就不会发sql语句

        //如果我们执行了session.clear()代码,表示一级缓存数据被清空了,那么这次获取的时候

        //还是要继续发出sql语句的

        Customer customer2 = (Customer)session.get(Customer.class, 1);

        

        System.out.println(customer2);

        session.getTransaction().commit();

        session.close();

    }

    

 

//refresh: 不管缓存中的数据是否发生了改变,将数据库中的数据重新放入缓存

    //如果缓存的数据发生了改变,那么这个改变将会被覆盖

    @Test

    public void testRefresh(){

        Session session = HibernateUtils.openSession();

        session.beginTransaction();

        

        Customer customer = (Customer)session.get(Customer.class, 1);

        customer.setName("rose");

        

        System.out.println(customer);

        //从数据库中根据OID重新加载这条记录,原先的改变失效,数据库中原始的值会覆盖修改的值

        session.refresh(customer);

        

        System.out.println(customer);

        

        session.getTransaction().commit();

        session.close();

    }

 

【面试题】请你说说session的flush和refresh的区别?

 

为什么要清除一级缓存:

大批量处理数据的时候,有些数据无需在一级缓存存在,或者已经处理完了,为了防止内存泄漏,一级缓存爆满,可以手动清除一级缓存的对象。

  1. get()和load()的区别

【状态变化】直接拿到持久态对象。

 

【理解 session的get方法和load方法区别】

两者的区别:

get()方法是立即加载,即执行get方法后,立即发出查询语句进行查询,直接返回目标对象

load()方法是延迟加载,即执行load方法后,不会立即发出查询语句,返回具有id的目标对象代理类子对象,再访问对象的除了ID之外的其他属性的时候,才发出SQL语句进行查询

【示例】

分别用get和load,来根据不同id查询不同对象,使用debug来查看语句发出的时机以及延迟加载时的代理类子对象。

@Test

    public void testGetAndLoad(){

        Session session = HibernateUtils.openSession();

        session.beginTransaction();

        

        //比较get和load的区别

        //1 观察get和load的sql语句发出时机

        

        //get方法立即发出sql语句

//        Customer customer = (Customer) session.get(Customer.class, 1);

//        System.out.println(customer);

        

        //load方法:属于懒加载,如果只用到id属性,是不会发出sql查询语句的

        //只有用到id以外的其他属性的时候,才会发出sql查询语句

        Customer customer = (Customer) session.load(Customer.class, 1);

        //简单的打印id,发出sql语句吗?

        System.out.println(customer.getId());

        //发出sql语句吗?

        System.out.println(customer.getName());

        

        session.getTransaction().commit();

        session.close();

    }                

 

C2引用的Customer的代理子对象

继续调试

初始化后,内部handlerinitialized变为true (已经初始化), target 指向真正查询结果对象

 

 

【关于load延迟加载的几个情况提示】

  • 代理子对象,handler属性,包含了主键id,因此只访问id时,不需要发出sql语句。

【示例】

延迟加载时,访问ID属性不发出sql,访问ID之外的属性时发出sql

//load:不会立即发出sql,只有在访问除id之外的其他属性的时候,才发出sql

        Customer customer2 =(Customer) session.load(Customer.class, 2);

        System.out.println(customer2.getId());//不需要发出sql

        System.out.println(customer2.getName());//此时才发出sql

 

  • 当id在数据库中不存在的时候,访问其他属性时会发生 ObjectNotFoundException。

【示例】

两者查不到数据的区别:

结论:get会返回null,load会报错:

 

【扩展了解】

代理子对象是谁来负责生成的?

答:通过javassist.jar来进行生成子对象(反射机制)

 

延迟加载的好处:节约资源,需要的时候再加载(提高内存利用率),如果不需要的话,先不加载。

 

 

  1. 多表关联映射配置和操作(未来项目肯定会用)

    1. 多表设计

多对多: 学生选课 (一个学生选多门课, 一门课被多个学生选择)

一对多: 客户和订单 (一个客户可以产生很多订单, 一个订单属于一个客户)

一对一: 一个公司对应建表规则

多对多: 一定产生第三方关系表, 需要三张表 (学生表、 课程表、 选课表), 关系表联合主键,引入两张实体表主键,作为外键

一对多: 在多方表,添加一方主键作为外键 ,不需要第三张表 , 需两张表(客户表 、订单表),在订单表添加客户id

    一对一: 在任意一方添加对方主键作为外键

一个地址

范式:可以理解成是数据库设计的规范/标准

为什么表的设计需要有规范和标准?

防止数据冗余,科学的数据库设计可以防止数据冗余

 

//一般数据库的设计要符合3NF,符合的范式要求越高,表越多

范式之间的关系:

 

第一范式(1NF):保证每列的原子性(每列都是不可再分割的单元)

学生表

stuno

stuinfo

Coursename

1

Lucy23

Java

1

Tom18

Oracle

1

Rose12

Hibernate

    上表是不符合第一范式的,修改如下

stuno

Name

Age

Coursename

1

Lucy

23

Java

1

Tom

18

Oracle

1

Rose

12

Hibernate

 

第二范式:保证表有主键

上表符合第二范式吗?不符合,因为没有主键,修改如下:

stuno

Name

Age

Coursename

1

Lucy

23

Java

2

Tom

18

Oracle

3

Rose

12

Hibernate

 

第三范式:每张表不包含其他表中非主键以外的字段(每张表的字段都依赖于当前的主键)

上表符合第三范式吗?答:不符合,修改如下

stuno

Name

Age

1

Lucy

23

2

Tom

18

3

Rose

12

 

Courseid

Coursename

1

Java

2

Oracle

3

Hibernate

 

关系表:

Stuno

Courseid

1

1

1

2

2

1

2

3

   

 

BCNF

4NF

5NF

 

  1. java对象(po)描述表关系

Hibernate 是一个完全ORM框架,使用hibernate 编程,可以完成类和表映射

多对多 :

    Student {

        // 一个学生对应 多门课

        // Set、List、bag、数组 代表复数,基本区别:set不能重复,没顺序、list可以重复,有顺序。bag:不能重复,而且有顺序(缺点:效率低)

        Set<Cource> cources ;

}

    Cource {

        // 一门课,多个学生

        Set<Student> students ;

    }

 

一对多 :

    Customer {

        // 一个客户 多个订单

        Set<Order> orders ;

}

    Order {

        // 一个订单 一个客户

        Customer customer ;

    }

一对一:

    Company {

        // 一个公司 一个地址

        Address address;

    }

    Address {

        // 一个地址 对应一个公司

        Company company ;

    }

 

我们下面重点学习一对多和多对多!

 

 

  1. 一对多关联映射配置

案例:客户和订单(一对多)

建立一个包用于测试: cn.itcast.b_oneToMany

 

  1. 映射配置

编写方法:实体类的编写,先写单表的属性和配置,再加关系。

 

【第一步】:实体类编写

 

【第二步】:hbm映射文件编写

Order.hbm.xml

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE hibernate-mapping PUBLIC

"-//Hibernate/Hibernate Mapping DTD 3.0//EN"

"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">

<hibernate-mapping>

    <class name="cn.itcast.b_onetomany.Order" table="t_order">

        <!-- 主键 -->

        <id name="id">

            <generator class="native"></generator>

        </id>

        <!-- 其他属性 -->    

        <property name="name"></property>

        

        <!-- 配置关系

             name:类中属性

             class:这个属性的原型(属性对应对象的完整的包路径)

             column:customer在Order表中的外键的名字

         -->

        <many-to-one name="customer" class="cn.itcast.b_onetomany.Customer" column="cid"></many-to-one>

        

        

    </class>

 

</hibernate-mapping>

 

Customer.hbm.xml

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE hibernate-mapping PUBLIC

"-//Hibernate/Hibernate Mapping DTD 3.0//EN"

"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">

<hibernate-mapping>

    

    <!-- class -->

    <class name="cn.itcast.b_onetomany.Customer" table="t_customer">

        <!-- 配置主键 -->

        <id name="id">

            <!-- 主键策略 -->

            <generator class="native"></generator>

        </id>

        <!-- 其他属性 -->

        <property name="name"></property>

        

        <!-- 配置集合

             name:类中的属性名

         -->

        <set name="orders">

            <!-- 配置当前Customer对象在order表中的外键的名字 -->

            <key column="cid"></key>

            <!-- 配置关系

                class:当前order对应的完整的包路径(集合中装载的数据的原型)

            -->

            <one-to-many class="cn.itcast.b_onetomany.Order"/>

        </set>

        

    </class>

 

 

</hibernate-mapping>

 

注意:两个配置文件的外键必须对应!!!!!

 

【第三步】:核心配置文件中添加映射

<!-- 配置一对多的映射文件 -->

        <mapping resource="cn/itcast/b_onetomany/Customer.hbm.xml"/>

        <mapping resource="cn/itcast/b_onetomany/Order.hbm.xml"/>

 

 

【第四步】:建表测试

 

    @Test

    public void createTable()

    {

        HibernateUtils.getSessionFactory();

    }

建表成功:

 

提示:外键的数据类型自动会使用对方主键的类型

 

 

  1. 相关操作

本节难点:cascade级联和inverse外键维护

  1. 保存操作Save

多表保存的原则:双方都必须是持久态的对象!

 

目标:学习几种保存方法。

  1. 双向关联保存

    @Test

    public void testSave(){

        Session session = HibernateUtils.openSession();

        session.beginTransaction();

        

        Customer c1 = new Customer();

        c1.setName("rose");

        

        Order o1 = new Order();

        o1.setName(c1.getName()+"的订单1");

        

        //双向建立关系

        c1.getOrders().add(o1);

        o1.setCustomer(c1);

        

        //必须同时保存两个对象,否则会报错

        session.save(c1);

        session.save(o1);

        

        session.getTransaction().commit();

        session.close();

    }

    

 

这种保存要求:必须双方都建立关系,而且都要执行保存操作.

 

  1. 级联保存

【需求】

保存客户的同时自动保存订单。

默认情况下:

会报错:

原因结论: 在hibernate代码中,在session.flush前,不允许 持久态对象 关联 瞬时态对象

持久态对象只能关联持久态对象!

解决:采用级联 ,cascade :cascade="save-update"

它的作用:

可以使持久态对象"关联"瞬时态对象, 自动会隐式执行save操作,变为持久态

可以使持久态对象"关联"脱管对象,自动会隐式执行update操作,变为持久态

 

如果通过操作customer来级联保存 order ,需要在Customer.hbm.xml(谁是持久的) 配置级联.

Customer.hbm.xml:

测试代码:

@Test

    public void testSave(){

        Session session = HibernateUtils.openSession();

        session.beginTransaction();

        

        Customer c1 = new Customer();

        c1.setName("lucy");

        

        Order o1 = new Order();

        o1.setName(c1.getName()+"的订单1");

        

        Order o2= new Order();

        o2.setName(c1.getName()+"的订单2");

        

        Order o3 = new Order();

        o3.setName(c1.getName()+"的订单3");

        

        Order o4 = new Order();

        o4.setName(c1.getName()+"的订单4");

          

        

        //当级联保存的时候,当我们在Customer.hbm.xml中设置了级联关系的时候,

        //那么在设置关系的时候,只需要向Customer的orders集合中添加Order,就可以进行级联保存

        c1.getOrders().add(o1);

        c1.getOrders().add(o2);

        c1.getOrders().add(o3);

        c1.getOrders().add(o4);

        

        

//        o1.setCustomer(c1);

        

        //必须同时保存两个对象,否则会报错

        session.save(c1);

//        session.save(o1);

        

        session.getTransaction().commit();

        session.close();

    }

 

使用级联之后,Hibernate会对瞬时态的这个对象,会自动执行save操作.

 

 

问题:如果保存顺序反过来呢?即先保存订单,同时保存客户呢?
分析:先操作order ,级联保存 customer ,需要在 Order.hbm.xml 配置级联

 

Order.hbm.xml :


//保存订单的时候,能不能自动的保存customer

    @Test

    public void testSave3(){

        

        Session session = HibernateUtils.openSession();

        session.beginTransaction();

        

        Customer c1 = new Customer();

        c1.setName("jack");

        

        Order o1 = new Order();

        o1.setName(c1.getName()+"的订单1");

        

        o1.setCustomer(c1);

        

        session.save(o1);

        

        session.getTransaction().commit();

        session.close();

        

    }

 

级联对于大量的保存或更新操作非常有用。

 

 

  1. 对象导航—连续保存—其实是依赖于级联。

    对象导航概念:通过一个对象的保存操作,可以自动导航到另外一个关联对象的保存操作。

下面有个面试题,前提:customer和order都配置了级联保存(双向都配置),那么请问下面的1,2,3语句分别产生几条插入的sql语句,

答案: 4 3 1

@Test

    public void testSaveByCascadeNavi(){

        Session session = HibernateUtils.openSession();

        session.beginTransaction();

        

        Customer c1 = new Customer();

        c1.setName("itcast2");

        

        Order o1 = new Order();

        o1.setName(c1.getName()+"的订单1");

        

        Order o2 = new Order();

        o2.setName(c1.getName()+"的订单1");

        

        Order o3 = new Order();

        o3.setName(c1.getName()+"的订单1");

          

        

        o1.setCustomer(c1);

        c1.getOrders().add(o2);

        c1.getOrders().add(o3);

        //第一种情况 4

        session.save(o1);

        //第2种情况 3

        session.save(c1);

        //第3种情况 1

        session.save(o2);

        

        session.getTransaction().commit();

        session.close();

    }

 

Hibernate的外键:是由关系来提供

导航保存对于大量的保存或更新操作非常有用。

 

 

  1. 删除操作delete

表之间的依赖关系:在一对多中 ,多方表(从表) 依赖 一方表(主表) (order依赖customer)。

 

  1. 删除多方的一条数据

 

        // 第一种情况:直接删除多方的数据

//        Order order = new Order();

//        order.setId(7);

//        

//        session.delete(order);

        

删除多方 (订单),直接删除

 

  1. 删除一方的一条数据(问:会如何呢?)

//第二种情况:直接删除一方的数据

        Customer customer = new Customer();

        customer.setId(4);

        //当customer是一个脱管态对象的时候,先解除关系,删除,所以删除之后,你会发现多方的外键被置空

        session.delete(customer);

删除一方(客户),被依赖

结果:多方的外键被置空了,一方被删除了(内部机制)

 

原理:Hibernate 先解除对一方外键依赖,然后进行删除

(如果外键设置 not-null , 无法删除 )课后可以试试

 

  1. 级联删除-删除客户自动删除对应的订单(知识点:这里就可以看出删脱管对象和持久对象的区别了

     

    级联删除必须是持久态对象才会有级联删除效果,否则是无效的

     

问题: 从关系型数据库的角度来说:在一对多数据模型中,多方对一方存在依赖的, 如果一方数据被删除,多方数据不完整,无意义。(客户删除的同时,将订单一块都删了)

解决方法: 在一方 配置 cascade="delete" ,会级联删除.

级联删除的要求:被删除的对象要是持久态的对象

 

    @Test

    public void testDelete(){

        Session session = HibernateUtils.openSession();

        session.beginTransaction();

        

        //删除多方的数据:直接删除

//        Order order = new Order();

//        order.setId(15);//设置oid,删除只能根据id删除

        

        // 删除

//        session.delete(order);

        

        //删除一方的数据

//        Customer customer = new Customer();

//        customer.setId(7);

        //当删除的时候,hibernate会先解除关系(将cid置空),然后在进行一方的删除操作

//        session.delete(customer);

        

        /**

         * 在进行级联删除的时候,如果删除的对象还是脱管态对象,级联删除失效,

         * 默认处理方式:先解除关系,再一方删除数据(多方数据还存在,并没有达到级联删除的效果)

         *

         * 级联删除:一方对象必须是持久态,这样子,才能实现级联删除的效果

         */

        //持久态

        Customer customer = (Customer)session.get(Customer.class, 6);

        //由于customer对象是持久态,所以会级联删除order订单

        session.delete(customer);

        

        session.getTransaction().commit();

        session.close();

        

    }

 

结论:删除操作中,删除托管对象没有级联效果删除持久对象可以进行级联删除

 

 

  1. 级联属性配置-cascade

Hibernate级联开发配置, cascade常用的取值:

  • save-update:对关联瞬时对象执行save操作,对关联托管对象执行update
  • delete:对关联对象进行删除

 

在实际开发中,一对多模型中,一方一般是主动的一方(多方要依赖一方),如果配置级联,通常在一方进行配置!!!

因为多方需要引用一方的主键作为外键使用

 

级联删除的情况:删除客户,订单也已经没有存在的意义了

删除订单,没有必要删除客户,没有必要再多方配删除级联

 

    实际项目开发中,一般级联主要是用来进行级联删除操作,很少用来进行级联保存。一般都是先有一方的数据,再有多方的数据,即先有客户,再有订单。所以,保存多方不配置级联(不在多方配置级联)。

    

 

总结:

一般在业务开发中,不要两端都配置级联,(多方尽量不要配置级联,尽量在一方配置级联

配置了级联之后,必须操作持久态对象,否则不会级联删除。

 

  1. 外键的维护问题-inverse

问题:多余sql的问题

示例:

将没有关系的一个客户和一个订单建立关系。(双方)

@Test

    public void testInverse(){

        

        Session session = HibernateUtils.openSession();

        session.beginTransaction();

        

//        获取5号客户

        Customer customer = (Customer)session.get(Customer.class, 5);

//        获取订单

        Order order = (Order)session.get(Order.class, 11);

        

        //添加双向关联关系

        customer.getOrders().add(order);

        order.setCustomer(customer);

        

        //此时当commit的时候,会隐式的flush

        

        session.getTransaction().commit();

        session.close();

    }

产生的sql:多余sql

 

分析图:

解决方法:

采用inverse属性来配置。

单的说这个属性谁是true就放弃了主键维护权

inverse默认值是false,即双方都有外键维护权。

inverse只对集合起作用,也就是只对one-to-manymany-to-many有效.

在业务开发中,一般是在一方放弃。(从业务场景上来分析,一般先存1 方,再存多方,那么就存多方的时候建立关系就比较合理。)

再次运行,发现就一条语句了:

一般,我们都让1方放弃外键维护权!

  1. 多对多关联映射

学习点:掌握多对多配置方式。

案例:学生和课程,学生选课

  1. 映射配置方式

    1. 多对多关系分析(student\course)

学生和课程 是经典 多对多关系,学生选课是关系表数据

    在多对多中,一般情况 没必要使用 cascade 级联的!!!

 

  1. 映射配置

【第一步】:实体类编写,创建cn.itcast.c_manytomany包,然后操作如下:

 

 

【第二步】:hbm映射文件编写

student.hbm.xml

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE hibernate-mapping PUBLIC

"-//Hibernate/Hibernate Mapping DTD 3.0//EN"

"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">

<hibernate-mapping>

    <class name="cn.itcast.c_manytomany.Student" table="t_student">

        <id name="id">

            <generator class="native"></generator>

        </id>

        <property name="name"></property>

      

        <!-- 配置集合

             name:类中的属性

             table:关系表名

         -->

        <set name="courses" table="t_s_c">

            <!-- 当前Student对象在关系表中的外键名字 -->

            <key column="sid"></key>

            <!-- 配置关系

                 class:当前的Course对应的完整的包路径

                 当前集合中装入的对象对应的完整的包路径

                 column:当前的Course对象在关系表中的外键

             -->

            <many-to-many class="cn.itcast.c_manytomany.Course" column="cid"></many-to-many>

        </set>

        

        

    </class>

 

</hibernate-mapping>

 

Course.hbm.xml

 

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE hibernate-mapping PUBLIC

"-//Hibernate/Hibernate Mapping DTD 3.0//EN"

"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">

<hibernate-mapping>

    <class name="cn.itcast.c_manytomany.Course" table="t_course">

        <id name="id">

            <generator class="native"></generator>

        </id>

        <property name="name"></property>

        <!-- 配置集合

         table:配置多对多关系的时候,两边的table要一致

         -->

        <set name="stus" table="t_s_c">

            <key column="cid"></key>

            <many-to-many class="cn.itcast.c_manytomany.Student" column="sid"></many-to-many>

        </set>

        

        

    </class>

    

 

</hibernate-mapping>

 

 

 

【第三步】:核心配置中添加映射

【第四步】:建表测试

建表完成之后,查看建表语句:

 

  1. 映射图解

图解:

只要是多对一的标签,都配置自己在对方的外键属性。

 

  1. 相关操作

    1. 保存操作save,同时建立关系

即保存学生和课程数据的同时,在中间选课表插入关联数据。

@Test

    public void testSave()

    {

        Session session = HibernateUtils.openSession();

        session.beginTransaction();

        //新建一个学生

        Student stu = new Student();

        stu.setName("rose");

        //新建课程

        Course course = new Course();

        course.setName("hibernate");

//        /双向关联

        stu.getCourses().add(course);

        course.getStus().add(stu);

          

        

        //发现抛出异常,如何解决

        //1.外键维护权,一方放弃inverse="true",并且不放弃维护权的一方,加入 cascade="save-update"

        //2.建立关系是,只需要建立一方的关系即可,并且建立关系的一方,加入 cascade="save-update"

        

        

        

        //双向保存

        session.save(course);

        session.save(stu);

          

        

        session.getTransaction().commit();

        session.close();

        

    }

    

错误:

 

可以采用两种方案解决问题:

第一种方案.外键维护权,一方放弃inverse="true",并且不放弃维护权的一方,加入 cascade="save-update":推荐方案

第二种方案.建立关系,只需要建立一方的关系即可,并且建立关系的一方,加入 cascade="save-update"

 

 

第一种方案:

执行的代码:

 

    @Test

    public void testSave1(){

        Session session = HibernateUtils.openSession();

        session.beginTransaction();

        //创建学生

        Student s1 = new Student();

        s1.setName("rose");

        ///创建课程

        Course c1 = new Course();

        c1.setName("hibernate");

        //建立双向关联关系

        s1.getCourses().add(c1);

        c1.getStus().add(s1);

        //保存

//        session.save(c1);

//        session.save(s1);

        

        //第一种方案:一方放弃外键维护权,另一方加入级联保存,未来保存的时候,在另一方进行保存操作

        session.save(s1);

          

        

        session.getTransaction().commit();

        session.close();

    }

 

第二种方案:第二种方案xml文件的配置直接采用第一种方案的配置,不做任何修改,此时,我们知道Student.hbm.xml中配置了

Cascade="save-update",所以接下来的测试代码如下:

 

实际业务中,一般不会去级联保存,因为学生和课程是各自产生存在的.

 

结论:多对多模型中,一次创建关系,对应中间表 一条insert语句 !不需要双方发生关系。

 

在实际业务中,要么是在一方建立关系,要么是如果两方都建立关系,就配置inverse,让一方主动放弃维护权。

我们推荐一方放弃维护权

 

  1. 解除关系

两个对象解除关系就是删除中间表的关系数据,即将两个对象解除关系。删除的时候,对象必须是持久态对象

@Test

    public void deleteRelation(){

        Session session = HibernateUtils.openSession();

        session.beginTransaction();

        

        Student student = (Student) session.get(Student.class, 3);

        Course course = (Course) session.get(Course.class, 3);

        

        //注意:由于course放弃了对集合的维护权,所以此时只能在Student这一方进行集合操作

        student.getCourses().remove(course);

        

        //不需要手动的删除,直接使用快照的更新功能,commit会隐式的flush

        session.getTransaction().commit();

        session.close();

        

    }

语句问题:查看语句。

 

  1. 改变关系:必须是持久态的对象

变更选课内容,原来选的语文,改为选数学

----- hibernate 无法生成update语句 (只能由我们自己先delete,后insert—先解除关系,再增加关系 )

分析:假如能update,你要update中间表,但中间表无法通过实体操作。

插入测试数据,-

@Test

    public void changeRelation(){

        Session session = HibernateUtils.openSession();

        session.beginTransaction();

        

        //让2号学生选修3号课程

        Student student = (Student) session.get(Student.class, 2);

        Course course = (Course) session.get(Course.class, 2);

        //解除关系

        student.getCourses().remove(course);

        

        //查找3号课程

        Course course2 = (Course) session.get(Course.class, 3);

        //添加关系

        student.getCourses().add(course2);

        

        //flush

        session.getTransaction().commit();

        session.close();

    }

 

 

  1. 删除实体:持久态对象

多对多的删除!!!!!!!!!!

一定不能级联删除!!!!!!!

 

 

删除学生的时候,Hibernate会自动删除关系表(中间表)的数据(无需级联

 

    @Test

    public void testDeleteEntity(){

        Session session = HibernateUtils.openSession();

        session.beginTransaction();

        

        Student stu = (Student)session.get(Student.class, 2);

        //删除

        session.delete(stu);

        

        //flush

        session.getTransaction().commit();

        session.close();

        

    }

    

 

注意: 多对多不建议使用 级联 (delete 级联),造成数据丢失 !!!!!!!(如果两边都配置级联,会两边删除,造成表被清空)

测试一下:(实际业务中是不存在的!!3个学生对应3个课程,创建9个关系,随便删除哪个对象,数据直接被置空

在课程或者学生方配置级联

如果在学生方面配置了级联,那么当删除持久态的学生对象时,会将对应的课程也同时删除掉了!从而造成数据丢失!(删除脱管的不会,因为脱管态对级联不起作用

(课后尝试多对多的级联删除)

 

  1. 小结+重点

 

复习:

1 能够说出PO对象三状态

2 理解session一级缓存(能够存储数据)

3 理解快照(用来更新数据的)

4 get和load的区别

5 掌握一对多的配置

6 掌握一对多的CRUD操作

7 掌握多对多的配置

8 掌握对多对的CRUD操作

 

学会debug,多用debug

 

  1. 作业

【作业一】

完成全天课程练习。

【作业二】

完成课前资料中的:《hibernate知识点作业练习》文档中的练习。

 

 

 

 

 

 

 

posted @ 2017-01-10 22:09  beyondcj  阅读(455)  评论(0编辑  收藏  举报