【JavaWeb-20】3种对象状态以及相互转化、一级缓存和快照、Session其他API、一对多配置和操作、cascade

1、对象状态的基本解释。

  1. 瞬时态|临时态,指没有与Hibernate产生关联(即没有用Hibernate保存修改对象等操作,本质上是在Session中没有缓存,后续说法有些不统一,在此说明),也没有与数据库中的记录产生关联(即对象与数据路中记录没有对应,即与数据库中的ID没有对应)。也就是临时创建的一个对象,没有做什么操作。
  2. 持久态,指与Hibernate和数据库有关联(即对象有ID)。
  3. 游离态|托管态,指没有与Hibernate产生关联,但之前可能产生过关联,比如我们关闭Session后这个对象就有可能从持久态变成游离态。对象有ID,即和数据库有关联。

2、我们来看下面一段代码,注释了每个阶段的对象状态。

    @Test
    public void test01(){
        Session openSession = HibernateUtil.openSession();
        openSession.beginTransaction();
        //瞬时态
        User u=new User();
        //瞬时态
        u.setUsername("eric");
        //瞬时态
        u.setPassword("1234");
        //持久态,虽然此时数据库中还没有,但是它未来是一定会被记录到数据库中的,所以被定义为持久态
        openSession.save(u);
        //持久态
        openSession.getTransaction().commit();
        //游离态
        openSession.close();
        //sf在工具类中自动关闭了
    }

——瞬时态=>持久态,只需要调用save方法即可。我们调用save方法并不是向数据库中插入数据,插入数据的操作是提交事务时候操作的,save做的事情就是获得对象的id。我们的主键生成策略是native,也就是说在数据库中自增,那么需要insert into插入数据库后获得id+1的新id,但是如果我们把主键生成策略改成increment,意思是由Hibernate来自增,那么这个时候程序到save的时候,其实是执行了select max(id) from t_user代码来获取最大id后+1得到新的id给我们的对象,因为只有对象获得了id才能进入持久化状态。进一步的如果我们主键生成策略变成assigned手动指定但是我们没指定,那么就会报错。

——瞬时态=>游离态。瞬时态是和Hibernate有关联但没有id;游离态是和Hibernate有关联但是有id。所以区别就在于从没有id的状态变成有id的状态,那么这个id就需要我们手动指定(id取值要和数据库中的id一致)。

//瞬时态
User u=new User();
//游离态
u.setId(2);

——持久态(有关联,有id)=>瞬时态(无关联,无id)。方法就是关闭session变成无关联,然后手动设置id为null。

//获得一个持久态的对象
User u1=(User) openSession.get(User.class,1);
openSession.getTransaction().commit();
//关闭Session,有id没关联,变成游离态
openSession.close();
//把id设置为null,变成瞬时态
u1.setId(null);

还有一种方法是不需要通过关闭session来取消关联,可以通过一个函数evict来取消关联。

//获得一个持久态的对象
User u1=(User) openSession.get(User.class,1);
//取消关联,有id,是游离态
openSession.evict(u1);
//把id设置为null,变成瞬时态
u1.setId(null);
openSession.getTransaction().commit();
openSession.close();

——持久态(有关联有id)=>游离态(无关联,有id)。可以在上面的例子里面获得(持久->瞬时会经过持久->游离->瞬时),也就是我们通过evict来取消关联,获取通过关闭Session来取消关联即可从持久态变成游离态。

——游离态(没有关联有id)=>瞬时态(没有关联没有id)。也就是直接设置id为null。这个也可以在上面的例子里面获得(持久->瞬时会经过持久->游离->瞬时)。

——游离态(无关联有id)=>持久态(有关联有id)。只要关联即可。

//获得持久态的
User u1=(User) openSession.get(User.class,1);
//解除关联,只剩下有id,所以变成游离态的
openSession.evict(u1);
//这个时候不能用save,因为id和表中原有数据冲突,只能用update,变成持久态
openSession.update(u1);

——我们需要尤其关注的是持久态的对象,因为我们要保存数据到数据库时要求对象时持久态的。而且持久态有个特点,就是如果这个对象时持久态的,那么它的一些变化,比如set方法设置值,这些变化会在事务提交的时候自动同步到数据库中也就是会自动调用update方法,不需要我们手动调用。

//这个u1是持久态的
User u1=(User) openSession.get(User.class,1);
// 持久态对象发生的改变会自动同步到数据库
u1.setUsername("andy");
//所以,下面这句代码是多余的不需要写的
//openSession.update(u1);

注意点:需要注意的是持久态的对象的id是不允许修改的,否则就报错,因为这个id是保持与session关联用的。

3、一级缓存和快照(了解原理)。

——Hibernate里面存在缓存,有2种,一种是线程级别的缓存叫Session缓存(就是一级缓存),另一种是进程级别的缓存,即Hibernate二级缓存(后面细说)。

——Session缓存,就是Session对象中存在的缓存,缓存的都是持久化的对象。所以我们之前说对象三种状态中有与Hibernate关联,其实本质上是与Session关联,我们说解除关联也是通过openSession.evict(*)来解除关联。

——证明Session缓存的存在。我们写下面3句代码,这些代码都是同一个意思,就是发送select语句取到数据->封装成对象->返回。但是我们发现只打印了一次select语句,原因就在此。第一次发送了select后把取到的封装好的对象(持久态的)放到了缓存中,以后再取得时候就检查缓存如果缓存里存在就直接从缓存里面拿而不是发送select语句到数据库里面拿。

User u1=(User) openSession.get(User.class,1);
User u2=(User) openSession.get(User.class,1);
User u3=(User) openSession.get(User.class,1);

——一级缓存以Map形式存在。Key就是对象的主键id(Serializable),Value就是这个对象本身。

——快照。下面两种更新方式,我们在不修改原有数据的前提下update,左边不打印update语句,但是右边会打印update(虽然右边我们的设置值和数据库中原有数据一致)。这就是快照的原因。
这里写图片描述

——我们通过get方法发送select语句从数据库取数据后,数据库返回数据的时候,我们的Session做了一系列事情,有我们之前说的Session负责封装数据成对象、把持久化的对象放入缓存外,还有一件事就是,它会把封装好的对象一式两份,一份放入缓存然后把缓存中的对象返回给程序使用,另一份就放在快照里。所以我们再提交更新的时候它会和快照进行比较,如果没有变化那么就不打印update语句,如果有变化再打印update语句。而右边的情况在提交事务的时候也会和快照进行比较,但是因为没有从数据库取过数据返回给Session,所以这个时候是Session里面是没有快照的,所以比较的结果就是不一致所以会打印update。

——缓存对效率的提升,一个直接的体现就在于结合了快照。比如我们获取了一个对象,一式两份有了一份快照,我们一顿修改这个返回的对象,修改了好多,最终提交事务的时候,它会统一所有的最终修改结果和快照比较,然后如果有变化就统一发送1条SQL语句,而不是向最原始的那种修改一次发送一次SQL语句,所以这就是缓存的作用。

——持久化状态:本质就是存在缓存中的对象,就是持久化对象。

——下面2个方法的功能是一样的,只是名称不一样,persist方法是sun公司命名的,它觉得persist更能体现持久化的功能,所以只是名称不一样。

openSession.save(u);
openSession.persist(u);

——HQL是否使用一级缓存?不用,因为我们写了下面3句代码,每次执行都重新发送select查询语句,所以它不使用一级缓存。

List<User> list1=openSession.createQuery("from User").list();
List<User> list2=openSession.createQuery("from User").list();
List<User> list3=openSession.createQuery("from User").list();

——HQL查询结果是否进入一级缓存?会进入一级缓存。我们如下代码,只打印出第一句代码的select语句,原因就在于我们第一句查询的结果放在了缓存中,第2句代码执行的时候直接从缓存中拿结果所以没有发送select语句。critieria的使用和HQL是类似的。

List<User> list1=openSession.createQuery("from User").list();
User u=(User)openSession.get(User.class, 1);

如果我们把查询数据变成下面这样,结果也是被放到一级缓存里了:

List<User> list=openSession.createSQLQuery("select * from t_user").addEntity(User.class).list();

但是如果是没有封装到对象里面的话,就不会放到一级缓存里。

List list=openSession.createSQLQuery("select * from t_user").list();

4、save和persist的区别之一体现。前提是我们的主键生成策略是native,也就是由数据库自增去维护。

User u=new User();
u.setId(20);
//不会报错,但是保存的对象的id不是20,而是由数据库自己维护的自增的id
openSession.save(u);
User u=new User();
u.setId(20);
//会报错,因为persist持久化的是完整的对象也就是说包括了id在内都持久化了,但是我们主键策略是native由数据库自己自增维护,所以产生了冲突,所以报错
openSession.persist(u);

如果要使用persist来自己设置id的话,我们把数据库里面主键id的auto_increment取消掉,然后把主键生成策略变成自定义的assigned,这样就能实现了。

5、HQL查询到底有没有使用一级缓存?我们之前的结论是说没有,因为它都是重新发送了select语句。但是本质上它其他使用了一部分。第1句代码执行的时候,它发送了select语句从数据库拿到数据,在Session里面封装成一个个User对象,放在缓存里面,再把结果集返回给程序。第2次查询的时候,虽然也发送了select语句去数据库拿数据,但是数据拿回给Session封装成对象的时候,Session一看自己缓存里面已经有了,所以这个时候第2次返回给程序的就是第1次生成的缓存。整个过程只是HQL查询的时候每次都经过了数据库而已。

6、缓存的小问题。如果我们之前查询了一个数据封装成对象放在缓存中,我们在中途修改了数据库,然后执行第2次查询的时候,它会优先使用缓存的数据,就会造成与数据库数据不一致。这种情况无法避免,但好在一级缓存的生命周期中openSession开始,到close结果,周期很短买这种中途被修改数据的几率较小。

7、Session的其他API。

——clear,清除缓存,所以下面的代码还是会打印2次select语句。

User u1=(User)openSession.get(User.class, 1);
openSession.clear();
User u2=(User)openSession.get(User.class, 1);   

——refresh,强制刷新缓存,解决和数据库中数据不一致的问题。它的原理就是,比如我们取了一个数据封装成对象放在缓存中,如果我们调用刷新方法,那么它就会强制缓存中的所有对象再通过发送select语句重新从数据库取得数据来和数据库数据保持一致,所以我们会看到又发送了一条SQL语句。

——flush,对比快照,并立即提交缓存对象。我们可以想象成手动的事务提交,因为我们修改了对象的属性,是要等到提交事务的时候才会把数据用update同步到数据库,但是如果你执行flush的话就会立即同步,不会等到提交事务的时候再同步。当然如果数据没有改变,flush也不会执行update同步。

——update和saveOrUpdate。

  1. 如果主键生成策略是native时,saveOrUpdate方法:如果主键为空,那么就执行insert,如果主键有值,那么就执行update。
  2. 如果主键生成策略是assigned,主键为空的时候会报错,因为无论是save还是update都必须指定id才能保存和修改。如果主键不为空的话,到saveOrUpdate时只会打印select查询语句,那么它会先查询看是否存在这个数据,在提交事务的时候如果不存在那么就insert,如果存在就比对数据执行update。

——我们在使用Hibernate的时候,要注意不要将2个相同id的对象放入缓存,因为会报错,而不是后者覆盖前者。

User u1=(User)openSession.get(User.class, 1);//u1持久态,在缓存中
openSession.evict(u1);//u1游离态,不在缓存中
User u2=(User)openSession.get(User.class, 1);//u2持久态在缓存中
openSession.update(u1);//u1持久态,但是无法放入缓存中,因为已经存在一个key(就是对象的id)相同的对象再缓存中(Map),所以报错

8、一对多。

——创建一个Customer对象,表示1。

public class Customer {
    private Integer id;
    private String name;
    private Set<Order> orders=new HashSet<Order>();
    //下面是3个setter和getter方法,省略
}

——创建一个Order对象,表示多。

public class Order {
    private Integer id;
    private String name;
    private Customer customer;
    //下面是3个setter和getter方法,省略
}

——然后写对应的xxx.hbm.xml文件,在Customer.hbm.xml中:

<hibernate-mapping package="com.hello.domain">
    <class name="Customer" table="t_customer">
        <id name="id" column="id">
            <generator class="native"></generator>
        </id>
        <property name="name" column="name"></property>
        <set name="orders">//这个name就是属性名
            <!-- 定义Customer在对方的外键引用名称,在Order.hbm.xml里会用到这个外键名称 -->
            <key column="cid"></key>
            <one-to-many class="Order" />
        </set>
    </class>
</hibernate-mapping>

——在Order.hbm.xml中,变化就在many-to-one标签处:

<hibernate-mapping package="com.hello.domain">
    <class name="Order" table="t_order">
        <id name="id" column="id">
            <generator class="native"></generator>
        </id>
        <property name="name" column="name"></property>
        <many-to-one name="customer" class="Customer" column="cid"></many-to-one>
    </class>
</hibernate-mapping>

——最后一个配置就是在hibernate.cfg.xml中新增这2个配置文件。然后新建一个类来测试新增数据:

@Test
    public void test01(){
        Session openSession = HibernateUtil.openSession();
        openSession.beginTransaction();

        Customer c=new Customer();
        c.setName("Andy");

        Order o1=new Order();
        o1.setName("wine");

        Order o2=new Order();
        o2.setName("biscuit");

        c.getOrders().add(o1);
        c.getOrders().add(o2);

        openSession.save(c);
        openSession.save(o1);
        openSession.save(o2);

        openSession.getTransaction().commit();
        openSession.close();
    }

执行后,数据库中就有了数据,其中3条是保存数据的插入操作,另外2条就是维护外键的,也就是在t_order表中维护cid外键:

Hibernate: 
    insert 
    into
        t_customer
        (name) 
    values
        (?)
Hibernate: 
    insert 
    into
        t_order
        (name, cid) 
    values
        (?, ?)
Hibernate: 
    insert 
    into
        t_order
        (name, cid) 
    values
        (?, ?)
Hibernate: 
    update
        t_order 
    set
        cid=? 
    where
        id=?
Hibernate: 
    update
        t_order 
    set
        cid=? 
    where
        id=?

——我们换一种方式新增数据,把上面2句换成下面2句代码,数据库中得到相同的数据,但是控制台打印的SQL语句却不同:

//      c.getOrders().add(o1);
//      c.getOrders().add(o2);

        o1.setCustomer(c);
        o2.setCustomer(c);

只有3条insert语句,没有2条维护外键的语句了。这说明我们的外键维护工作目前是由1的那一方在维护,也就是说Customer在维护,比如涉及到删除c时,c因为是Customer类,所以c会在删除的时候自动维护在o1和o2种引用的外键,把它们的外键设置为null,因为c都被删除了,没有引用了,这些外键的修改维护工作都是Customer默认在维护。

Hibernate: 
    insert 
    into
        t_customer
        (name) 
    values
        (?)
Hibernate: 
    insert 
    into
        t_order
        (name, cid) 
    values
        (?, ?)
Hibernate: 
    insert 
    into
        t_order
        (name, cid) 
    values
        (?, ?)

——我们可以修改,让Customer放弃维护外键。inverse属性默认是false就是维护外键不反转,设置成true之后,维护外键的工作反转给另一方,Customer放弃维护。这个时候如果删除了c的话,那么会报错,因为Customer不维护外键了,那么o1和o2还引用这个c,c已经被删除,所以出现了引用错误,所以报错。

<set name="orders" inverse="true">
    <key column="cid"></key>
    <one-to-many class="Order" />
</set>

——删除操作是:

Customer c=(Customer) openSession.get(Customer.class, 1);
openSession.delete(c);

——那如果我们确实让Customer放弃了维护外键,这个时候如果我们删除了c,就需要手动去维护外键,也就是把引用了这个c的地方的cid都设置为null。

Set<Order> set=c.getOrders();
for(Order o:set){
    o.setCustomer(null);
}

——放弃维护外键只能是1的那一方放弃。什么时候放弃这个要看具体业务,如果经常需要通过Customer去维护外键的话,就不要放弃。

9、cascade属性。

——我们看之前的代码,我们既然已经把o1和o2都添加到c的orders里面去了,如果我们直接保存c的时候它能帮我们把o1和o2也自动保存就好了,这样就不用我们写最下面的两句代码再自己保存o1和o2了。如果我们不做任何配置的时候,我们直接删除最后2句代码会报错,这个时候数据库会创建2个表,但是表里面没有任何数据。

c.getOrders().add(o1);
c.getOrders().add(o2):

openSession.save(c);
//openSession.save(o1);
//openSession.save(o2);

——这个时候我们需要配置cascade属性,其中有一个值叫做save-update,意思就是级联保存和修改。也就是说保存一个的时候其他相关的也会被保存。

<set name="orders" cascade="save-update">
    <key column="cid"></key>
    <one-to-many class="Order" />
</set>

——我们来修改一下试试联级修改能否有效。貌似有没有添加cascade="save-update"结果都一样。

Customer c=(Customer) openSession.get(Customer.class, 1);
for(Order o:c.getOrders()){
    o.setName("hello");
}

——级联删除:delete。删除Customer的同时删除这个Customer对应的Order。需要注意的是:如果我们在1方和多放都把cascade设置为delete,那么结局很残暴,数据将来回被删,直到删光两个表为止。

Customer c=(Customer) openSession.get(Customer.class, 1);
openSession.delete(c);

——级联孤儿删除delete-orphan。解除关系,同时将Order删除,Customer依然存在。

        Customer c=(Customer) openSession.get(Customer.class, 1);
        Iterator<Order> it=c.getOrders().iterator();
        //这里是在维护外键,也就是是移除外键,解除Customer与Order的关系,Order成为孤儿,后面再事务提交的时候Order会被删除
        while(it.hasNext()){
            it.next();
            it.remove();
        }

——all(sava-update+delete)。

——all-delete-orphan(save-update+delete+delete-orphan)。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值