版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
1讲hibernate介绍 软件分层最好层和层之间是单向依赖,自上而下,上层依赖下层,但不能下层依赖上层,而且层和层之间最好依赖抽象(面向接口编程),当下层的实现发生改变时,上层是不知道的,上层也不需要知道,这样上层就不需要改代码。也就是下层发生改变,不需要改变上层。 struts框架属于表示层,hibernate属于持久层。原来我们在持久层用的是jdbc语句,来操作数据库。现在用hibernate代替jdbc操作数据库。 hibernate是对jdbc进行了封装,底层还是jdbc在操作数据库。 为什么会产生hibernate这个框架呢?原来用jdbc开发持久层,它有很多冗余的东西(重复代码),它还需要管理connection对象。 第二个原因,java语言是面向对象的,是对象模型。而数据库大部分产品是关系型数据库。我们需要把对象和关系进行映射。用jdbc就会出现阻抗不匹配。hibernate最核心的功能就是做了这件事,将对象映射到关系型数据库里,俗称O/RMapping。 我们在开发中就更加面向对象了,OOA(面向对象分析)、OOD(面向对象设计)、OOP(面向对象编程)让实际操作更加趋于理论化(更加趋于对象)。 hibernate的另一个好处是移植性非常好,如果我们用了hibernate的标准用法,那么我们更换数据库产品就根本就不需要修改代码。 hibernate是轻量级的,没有侵入性,可以把任何POJO(纯java对象,没有继承任何类或实现任何接口)对象保存到数据库里,它也不需要容器的支持,这样就测试和开发起来就简单化了。 O/RMapping对象持久化产品还有oracle公司的Toplink、sun公司的JDO、Apache公司的OJB、sun公司的EJB2.0以前版本(它有一个CMP容器管理的持久化,这是重量级的,java对象必须继承一些东西,才能保存到数据库里,还必须依赖容器),这个现在几乎没有人用了。 现在EJB3.0规范有一个JPA规范,java持久化API,它不是产品,hibernate实现了JPA。 IBatis框架不是ORM框架,而是描述一个sql语句对应相关的对象,在代码编写中还是需要写sql语句。严格来说Ibatis是一个SQL映射。hibernate优点: 1提高了生产力,因为可以直接查询出对象,这样就不需要写代码将结果集封装对象,这样就减少了代码量。 2使开发更加对象化,我们不需要关心表,我只关心对象模型,hibernate会根据对象模型自动生成数据库表。不会出现阻抗不匹配,因为hibernate会把对象模型的关系映射到关系型数据库里。比如对象的继承,但数据库中没有继承关系,这就是阻抗不匹配。hibernate会让数据库的主外键关系模拟对象模型的各种关系。 3可移植性 4没有侵入性。 hibernate缺点: 1不方便使用数据库产品提供的特性功能,比如触发器等等,很难调优。因为封装太彻底。 2不适合大批量更新,因为有缓存。 3不适合做大量的统计查询功能。 使用hibernate就是使我们采用对象化思维操作关系型数据库。2讲hibernate包结构 doc目录里都是一些api,eg目录是hibernate的一个小例子,etc目录有核心配置文件以及第三方产品的配置文件模板(比如hibernate.cfg.xml、perties等文件),lib目录是hibernate依赖第三方的jar文件,hibernate3.jar是hibernate核心实现jar文件,src目录就是hibernate核心实现源代码,test目录是hibernate的测试程序。 hibernate和spring都是采用dom4j解析xml配置文件的。3讲第一个示例环境搭建 加入/lib/*.jar文件,这是hibernate所有依赖的第三方jar文件。 加入/hibernate3.jar,这是hibernate核心的jar文件。 加入数据库驱动。 加入hibernate核心配置文件。也可以用perties,但现在不常用。核心配置文件要放到src类路径下。<hibernate-configuration> <session-factory>//配置数据库的驱动 <propertyname="hibernate.connection.driver_class">com.mysql.jdbc.Driver</property>//配置数据库连接信息(URL) <propertyname="hibernate.connection.url">jdbc:mysql://localhost:3306/hibernate_basemapping</property>//配置数据库用户名 <propertyname="hibernate.connection.username">root</property>//配置数据库密码 <propertyname="hibernate.connection.password">bjpowernode</property> //数据库方言(相当于适配器)<propertyname="hibernate.dialect">org.hibernate.dialect.MySQLDialect</property> </session-factory></hibernate-configuration> 为什么要数据库方言呢,比如我们分页时,mysql采用limit,oracle采用rownum,但是hibernate代码是一样的,hibernate就是要根据数据库的方言来选择分页的选择。方言也就是相当翻译的功能,把hibernate代码翻译成数据库底层对应的sql语句。还有一些其它功能需要用的方言以后会慢慢讲到。//oracle方言4讲建立User实体类_映射实体类 实体类的规则:就是普通的java类,并为所有属性生成get/set方法。 为实体类创建实体映射文件,将类映射成表,属性映射成字段。一般建议映射文件和实体类放在同一个包里,因为它们是一体的,没有对立存在的意义。<hibernate-mapping>//实体类映射到表 <classname=""table="t_user1">//映射主键标识 <idname="id"> <generatorclass="uuid"/> </id>//映射普通属性 <propertyname="name"/> <propertyname="password"/> <propertyname="createTime"/> <propertyname="expireTime"/> </class></hibernate-mapping> class标签里的name表示实体类完整路径,table表示创建一个表,指定表名,用关系模型来存放对象模型。如果没有table也会创建表的,只是表名和类名一样。 id标签映射实体对象的主键标识,column指定主键标识映射到数据库里的字段名,如果没有column,那么字段名就和实体对象的主键标识相同。 普通属性映射用property标签,也可以用column指定属性映射到数据库里字段名称,如果没有指定就和属性同名。5讲将User导出数据库 上面讲了写映射文件,写好了还要把映射文件放到hibernate配置文件里。<mappingresource="com/bjpowernode/hibernate/User.hbm.xml"/> 要写全路径,也就是写上包名,不能用.了,要用/。 编写工具类ExoprotDB.java,把hibernate配置文件里的内容,生成DDL(数据库定义语言)语句,也就是hbm2ddl。publicstaticvoidmain(String[]args){ //默认读取hibernate.cfg.xml文件(将配置文件转成了一个对象) Configurationcfg=newConfiguration().configure(); //根据配置文件生成DDL语句 SchemaExportexport=newSchemaExport(cfg);//第一个true表示把DDL语句打印到控制台//第二个true表示把DDL语句生成到数据库 export.create(true,true);} 如果不写configure(),则会默认读取perties文件。如果hibernate配置文件改名了,则在configure()里写上文件名,在类路径下读取,我们把配置文件放到类路径下。6讲将User对象保存到数据库表中 访问那个数据库,就必须读取配置文件,获取数据库信息,获得Configuration对象。根据Configuration对象获取SessionFactory对象。一个SessionFactory对象就相当于一个数据库,相当于一个数据库的镜像,它只创建一份,是线程安全的。 sessionFactory有session工厂的作用,负责创建session对象。 然后根据sessionFactory获取session对象,这个session不是web里那个session对象,这个session对象是封装了Connection对象,我们操作一个session就关联了一个Connection对象,它可以管理Connection对象。用session对象就可以完成数据库的增删改查了。publicstaticvoidmain(String[]args){//读取hibernate.cfg.xml文件Configurationcfg=newConfiguration().configure();//建立SessionFactorySessionFactoryfactory=cfg.buildSessionFactory();//取得sessionSessionsession=null;try{//创建session对象session=factory.openSession();//开启事务session.beginTransaction();Useruser=newUser();user.setName("张三");user.setPassword("123");//保存User对象session.save(user);//提交事务session.getTransaction().commit();}catch(Exceptione){e.printStackTrace();//回滚事务session.getTransaction().rollback();}finally{if(session!=null){if(session.isOpen()){//关闭sessionsession.close();}}}},在eclipse中显示两个,另一个是hibernate2版本的包。 在hibernate.cfg.xml配置文件里添加一个属性,可以把生成sql打印到控制台。<propertyname="hibernate.show_sql">true</property> 也可以添加下面属性,把sql格式后打印到控制台。<propertyname="hibernate.format_sql">true</property> 在开发时最好加上上面两种的其中一个,可以方便我们查看生成的sql语句,方便我们调试。 另外再配置一个log4j的配置文件perties,放到src类路径下。我们暂时不管这个文件的配置,我们只需要在hibernate下etc目录下找到默认的perties文件放到src下就可以了。 这个配置文件里配置了很多log4j功能,如果想不要一些功能,就可以在配置文件里用#注释了,那么就不会有这个功能了。比如不要debug功能,就可以把debug注释了。 这样就方便程序的调试,如果有异常就会打印到控制台。7讲回顾hibernate的优缺点 POJO:纯java对象,没有继承任何类或没有实现任何接口。 详情见1讲的内容。8讲解释hibernate第一个示例程序 首先要读取配置文件,这样就会知道操作那个数据库。 SessionFactory对象就是一个工厂,用来产生session对象的,它代表一个数据库,可以把它理解成是一个数据库的镜像。并且它里面还有一些缓存,比如存放生成的sql语句。 SessionFactory对象最好只创建一次,一个数据库对应一个sessionFactory对象,它是线程安全的,所以可以把SessionFactory做成员变量。 用sessionFacory工厂创建session对象,用session对象就可以使用hibernate来完成数据库的增删改查操作。hibernate是对jdbc的封装,所以session必须关联connnection对象,因为底层还是connection在操作。但是session并不是一直关联connection对象,而是在真正要操作数据库时才会和connection对象关联。并且使用完了后session把connection对象放回池里。说白了session是可以管理connection对象的。9讲解析hibernate核心对象 用hibernate开发是从实体类开始的,先写实体类,然后写映射文件,然后使用hibernate的工具类SchemaExport,调用它的create方法就会根据映射文件把表自动创建到数据库里,这样就是更加面向对象。 但是在实际开发中,一般还是先建表然后创建实体类,写映射文件。这种方式还是比较多一些,因为数据库产品大部分是关系型数据库。 VO数据传输对象,PO持久化对象,POJO纯java对象。基于POJO的编程的都是轻量级的,hibernate就是轻量级的。但struts1就不是轻量级的,它不是基于POJO的编程,因为编写struts1的action或actionForm都不是基于POJO的编程,因为要继承struts1的类。spring和ejb3也是基于POJO编程,没有污染,对象不依赖其它的东西。ejb2不是基于POJO的编程。 下面是hibernate核心接口图: 不建议使用部分是hibernate2.x以前的版本,现在不建议使用了。 JNDI:是SUN公司提供的一种标准的Java命名系统接口,JNDI提供统一的客户端API,通过不同的访问提供者接口JNDISPI的实现,由管理者将JNDIAPI映射为特定的命名服务和目录系统,使得Java应用程序可以和这些命名服务和目录服务之间进行交互。 JNDI是一个应用程序设计的API,为开发人员提供了查找和访问各种命名和目录服务的通用、统一的接口,类似JDBC都是构建在抽象层上。 JNDI可以访问DNS、XNam、Novell目录服务、LDAP(LightweightDirectoryAccessProtocol轻型目录访问协议)、CORBA对象服务、文件系统、WindowsXP/2000/NT/Me/9x的注册表、RMI、DSMLv1&v2、NIS等目录及服务。 JNDI主要有两部分组成:应用程序编辑接口和服务供应商接口。应用程序编程接口提供了Java应用程序访问各种命名和目录服务的功能,服务供应商接口提供了任意一种服务的供应商使用的功能。JNDI可以管理对象,就像电话本一样,里面放了一个友好名又放了一个对象。比如tomcat里的连接池和它的集成就是JNDI。tomcat默认使用的连接池是DPCP,对我们来说我们是不知道的,我们访问的是JNDI服务,用lookup()方法,传入一个名字就可以拿到对象,转换成DataSource对象。我们也不知道DataSource是谁实现的。JNDI屏蔽了创建对象的过程,相当于工厂。就到注册薄里取就是了。它是以目录数形式来实现的,就像我们存文件的目录结构。JNDI大部分应用在分布式应用,特别是EJB。把对象注入进去,谁要用只需要用lookup查找就可以了。 JTA:Java事务API,也是sun公司规范,需要容器的支持,JTA相当于一个容器。可以实现多个数据库直接的事务管理(跨数据库的事务)。它是为两个阶段的提交。JTA容器会把各个数据库的conn对象放到JTA容器里来统一调度管理,统一的开启事务,如果conn对象调用都没有问题,那么JTA容器就统一提交事务。如果某一个conn提交失败了,就会发一个消息给另一个conn对象,让它和自己一起回滚。 只有符合XA规范就可以,可以利用JMS消息来做。10讲回顾上午内容11讲对象的三种状态 TransientObjects:暂态对象 PersistObjects:持久对象 DetachedObjects:游离态对象 我们new一个对象,没有保存到数据库里,也就是只有内存里有,数据库里没有,这个对象就是暂态对象。(也就是没有被session管理) 我们调用save方法保存一个对象,那个数据库里有,内存里也有,这时对象就是持久化对象。(被session纳入管理里) 如果我们对持久化对象,调用close方法把session关闭,或调用evict(一次清除一个对象)和clear(清除session里所有对象)方法清除session里的对象,那么持久化对象变成了游离态对象。(数据库里有,内存里没有,也就是不受session管理),游离态也是数据库有内存里也有,只是不受session管理。12讲Junit简介 我们创建一个目录存放测试代码,在eclipse里不能创建Folder,而是要创建SourceFolder源代码目录。 Junit有一个断言的概念,我们可以设置一个预期值,如果实际值和预期值不同,则就会显示测试通过还是不通过。 如果不用断言,那么我们需要用肉眼来判断结果是否通过测试。this.assertEquals(expected,actual); expected是预期值,actual是测试程序的实际值。13讲对象三种状态的演示 sessionFactory只创建一次,所以我们可以封装一下,提供一个方法直接返回session就可以了。 创建HiberanteUtils.java工具类privatestaticSessionFactoryfactory;static{ try{ //读取hibernate.cfg.xml文件 Configurationcfg=newConfiguration().configure(); //建立SessionFactory factory=cfg.buildSessionFactory(); }catch(Exceptione){ e.printStackTrace(); }}//返回session对象publicstaticSessiongetSession(){ returnfactory.openSession();}//关闭sessionpublicstaticvoidcloseSession(Sessionsession){ if(session!=null){ if(session.isOpen()){ session.close(); } }}//返回sessionFactory对象publicstaticSessionFactorygetSessionFactory(){ returnfactory;} sessionFactory只创建一次,我们把创建代码放到static块里。 为什么我们写hibernate代码没有提示我们处理异常呢?其实是有异常的,只是从hibernate3开始,hibernate的异常全部是运行期异常(继承RuntimeException)。所以不会强制我们手动try/catch来处理。但是我们最好还是手动用try/catch来处理。 或者是一直抛,在控制层里处理异常。 在测试类里就可以得到session了。Sessionsession=null;Transactiontx=null;try{ session=HibernateUtils.getSession();//开启事务 tx=session.beginTransaction();//创建User对象(暂态)Useruser=newUser();user.setName("张三");//保存暂态对象session.save(user);//对User对象名称修改user.setName(“李四”);//提交事务mit(); 调用session的save方法就是把User对象保存到session里(User对象纳入session管理),在session里有一个map,把对象保存到map里,key是保存对象的标识,value就是保存的对象。 注意:保存到session的对象并不是一定就马上保存到数据库里,真正保存到数据库里是提交事务后才真正保存到数据库里。因为提交事务就默认调用flush()方法,flush()方法才会去把对象保存到数据库里。 注意:提交事务后,发现数据库里user的名称改成李四了,一个持久化对象的属性发生改变,不用再save保存了,hibernate会自动检测到,自动去修改数据库里的记录。 注意:save方法是有返回值的,查看源代码发现返回类型是Serializable,也就是调用sava方法就会生成对象标识,并且返回,但是对象标识的类型必须是实现了Serializable接口的类型。 我们刚new一个对象,这个对象的标识是null,也就是没有标识。如果调用save方法,那么这个对象的标识就生成了。 调用save方法后,数据库里并不一定就保存了这个对象,但是我们仍然说这个对象是持久化对象。如果对象标识是靠数据库生成的,那么调用save方法后,数据库会马上保存。但是如果对象标识是采用uuid生成,那么save方法执行后,并不马上保存到数据库里,也就是查看数据库里是没有记录的。以后会详细讲。 执行上面代码,我们从控制台也可以看的出来,就是执行到user.setName(“李四”);修改对象属性后,都没有发出sql语句,那么这时数据库里肯定是没有的。当执行mit();提交事务,才看到有sql语句,也就是说这时才保存到数据库里。 上面代码生成了两条sql语句,第一条sql语句是插入记录。因为我们对持久化对象进行了修改,那么就插入第二条sql语句,修改刚插入的记录。 我们在提交事务时,其实做了一件事:清理缓存。也就是调用了flush方法。也成为同步数据,就是把session有的对象,而数据库里没有的,同步到数据库里。 当持久对象的属性发生改变时,hibernate在清理缓存时,会和数据库进行数据同步。把改变的值更新到数据库里。 我们在上面代码中的user.setName(“李四”);后面加上session.update(user);也就是手动调用update方法,结果和上面一样的,发出的sql语句也一样,所以结论是:如果持久对象的属性发生修改,调用不调用update方法,session都会同步到数据库里,所以没有必要手动调用update方法了。 如果在上面代码中,在finall代码块后面写上代码user.setName(“王五”);也就是在session关闭后执行这句代码。这是数据库里没有更新,只是在内存里更新了,因为这时的user对象是游离态的对象,不受session管理,所以session无法同步到数据库里。 如果要保存游离态对象的属性到数据库里,必须还得创建一个新的session对象,重写开启事务,然后调用session的update的方法,这样就会同步到数据库里。update方法的作用是把游离态对象再次纳入到session里管理(此时的user对象又变成了持久对象),然后提交事务,最后还别忘了关闭session。 session里有个缓存,刚开始是空的,它采用快照比对的方式,类似于给内存拍个照片,然后往里放入对象后,再次排张照片,当commit提交事务时,就会去对比两块内存的照片,如果不一样(也称有脏数据),就会调用flush方法清理缓存(也称脏数据检测),同步到数据库里。并且对象属性的每一次修改都会拍一张照片。 当重新创建session对象时,那么这个session的缓存照片还是空的,当我们把user对象纳入session管理后,这时的内存照片是user对象的内容,session会对比两个照片的数据,发现name属性发生改变了,并且user对象有id(对象标识),那么当提交事务后,清理缓存时保存到数据库里,因为user对象有id,所以不会再向数据库里插入数据,而是发生更新sql语句更新数据。14讲get方法加载对象 hibernate和缓存结合的很紧密,特别是sql语句的生成。根据缓存比对(快照比对)生成相应的增删改的sql语句。 get方法是从数据库里查询一个对象,第一个参数是Class类型,第二个参数是Serializable类型(主键),也就是根据主键来查询实体对象。并且get方法获取的对象是持久化对象。如果没有找到返回null。Useruser=(User)session.get 也就是我们用get方法查询的对象(持久化对象),我们可以直接修改它的属性,不用手动调用update方法,它会自动同步到数据库里。 调用get方法,就是发出select查询sql语句。是马上发出sql语句查询数据库。 我们用get方法查询的对象,如果调用对象的属性或方法之前首先要判断是否为null,否则调用对象的属性或方法时会发生空指针异常。用get方法查询找不到的对象时,并不会抛出异常,而是返回null。如果再调用对象的属性或方法时就会抛出空指针异常。 get方法没有使用泛型,所以我们需要强制类型转换。get方法永远不支持懒加载。15讲load方法加载对象 load方法也是要根据主键id查询对象。当我们调用load方法查询一个对象时,我们发现控制台并没有发出sql语句,而是产生了一个代理对象。这个代理不是jdk的代理,而是第三方工具CGLIB代理。CGLIB代理继承User类生成一个代理对象,此时的user对象并不是真正的User对象,而是User类的代理对象。Useruser=(User)session.get(User.class,"54434343434343");System.out.println(user.getName());session.getTransaction().commit(); 为什么load方法创建代理对象呢,不马上发出sql语句访问数据库查询对象呢?因为load方法支持懒加载,这也是一种提高性能的方式。我们查询对象时不去真正查询,而是创建代理对象,等真正调用这个对象的属性或方法时,才会去真正查询数据库。 当执行到user.getName();方法后才会真正发出sql语句去查询数据库。延迟加载就是靠代理实现的。当真正使用该对象时,才会发出sql语句创建(加载)对象。 为什么hibernate代理实现不是采用jdk提供的动态代理呢?因为jdk提供的动态代理,只能对实现了接口的类生成代理对象,我们的实体类是POJO对象,没有继承任何类和实现任何接口,所以无法实现,只能采用CGLIB代理。CGLIB采用的是继承方式来生成代理对象。 load方法默认是支持懒加载。 如果load方法传入的id数据库中没有,也就是查不到对象,这时的user对象也不是null,这和get方法不同,这时的user对象是CGLIB代理对象。也不会出异常。当执行到user.getName()时,也就是真正调用user对象时才会出ObjectNotFoundException异常,在异常前控制台依然可以看到hibernate发出了sql去查询。 也就是说load方法查询一个对象不存在,会抛出异常的。16讲delete方法删除对象 删除必须删除数据库中有的数据,那么也就是只能删持久对象和游离对象。所以在删除前必须获取一个持久对象或游离对象。 我们可以通过get或load方法先查询对象,然后调用delete方法删除。 我们可以通过new来创建游离对象,我们先new一个对象,这时是暂态,我们再把数据库中存在的id赋给这个对象,这时对象就是游离态了。对象状态的变化最终还是以数据库里有没有为准则。Useruser=newUser(); 如果id在数据库中存在,这时的user对象就是游离态对象。这就是手动构造游离态对象。 一般还是先加载,再删除。不会去手动构造一个游离态对象。17讲update方法 持久对象会自动更新数据库,游离态对象需要手动调用update方法同步到数据库。暂态对象就不能更新,因为数据库里都没有,怎么更新呢。 调用update方法发出的sql语句是update更新语句,并且将所有字段都用占位符,比如:updateUsersetname=?,password=?,createTime=?,expireTime=?whereid=? 这样写有好处:就是无论更新多少次只预编译一次sql语句,其它都是占位符的变化。缺点是:如果我们在调用update方法前如果没有赋值的属性,那么将会在数据库里更新为null。(这个原则只是对更新游离态对象)。也就是说如果是游离对象调用update方法就会更新数据库中所有的字段,所以没有更新的属性就会在数据库里更新为null,这样是很不安全的做法。 所以一般建议更新对象时,还是先用get或load方法先加载对象,然后再更新(这样也不用调用update方法了)。这样更新的话,对象中没有修改的属性值,会原封不动的赋给发出的sql语句中的占位符。这样就不会出现没有修改值的属性为null。 持久对象修改属性,可以调用update方法,也可以不调用,都会自动同步到数据库里。18讲总结三种状态和几个方法 编写Junit测试类时,类名最好用XXXTest,需要继承TestCase类。 编写单元测试方法名必须以test开头。没有参数没有返回值,并且是public。 并且建议使用断言。 session说白了就是一个持久化管理器。补充save和persist的区别、update和merge的区别、saveOrUpdate save和persist方法区别:在没有开启事务的情况下,保存一个对象时,save方法会发出sql语句把数据插入到数据库里,然后发现没有开启事务就会回滚,取消插入的数据。而persist方法如果没有开启事务,它根本就不会发出sql语句操作数据库。 update和merge区别:update方法是对游离态对象进行更新,并且将游离态变成持久对象,会发出sql语句更新数据库。 merge方法分两种情况,一是如果session中存在相同持久化标识(identifier)的实例,用客户端给出的对象的状态覆盖旧有的持久实例;二是:如果session没有相应的持久实例,则尝试从数据库中加载,或创建新的持久化实例,最后返回该持久实例。客户端提供的对象依然是脱管的,并没有被关联到当前session上。需要特别说明的是,使用update的时候,执行完成后,客户端提供的对象A的状态变成持久化状态,但使用merge的时候,执行完成,客户端提供的对象A还是脱管状态,hibernate或者new了一个B,或者检索到一个持久对象B,并把我们提供的对象A的所有的值拷贝到B,执行完成后B是持久状态,而客户端提供的A还是托管状态。 Merge更新实体对象时,该实体对象依然是游离态。19讲Query接口 get和load方法只能查一个对象,并且只能以id查询,如果要查询数据库表中所有的数据,那就需要Query接口。 使用Query接口就需要使用HQL语句来查询数据库中所有的数据并封装成对象。 HQL是hibernate提供的对象查询语言。Queryquery=session.createQuery("fromUser");ListuserList=query.list(); List里保存了数据库中user表中所有的数据并且封装成了对象。注意from后面不是表名而是实体类名。就是查询User类所有的对象,也就是把User类映射的表中的数据全部查询出来。hibernate分页:Queryquery=session.createQuery("fromUser");//从那条记录开始查(第一条记录是0,从0开始)query.setFirstResult(1);//每页显示多少条记录query.setMaxResults(4);ListuserList=query.list();20讲回顾内容 Interceptor拦截器,hibernate提供的拦截器,我们可以写拦截器程序,拦截器类似于过滤器。(事件监听) UserType很少用,是hibernate自定义一套类型。21讲基本映射 实体类映射成数据库的表,采用<class>标签映射。 实体类的普通属性(不包括集合、自定应类、数组)映射成数据库表的字段,采用<property>标签。 注意:如果实体类的类名、属性名如果和数据库的关键字冲突,那么无法映射成功,可以采用table来指定映射的表名,column指定映射的列名。 如果没有用table和colunm指定表名和列名,则表名默认用类名,列名默认用属性名。 实体类设计原则:1实现无参构造,因为hibernate会调用无参构造创建实体对象。2实体类不能用final修饰,懒加载的实现是由CGLIB代理,它会继承实体类,所以创建代理对象时会去创建父类对象的,所以实体类不能用final修饰。3实体类要提供主键标识。4建议提供set和get方法。我们在hibernate3.jar包可以找到hibernate.cfg.xml配置文件的dtd,名称为hibernate-configuration-3.0.dtd。映射文件的dtd为hibernate-mapping-3.0.dtd。dtd文件里定义了配置文件中标签的作用。我们可以在hibernate提供的中文文档第五章查看,我们只讲常用的,如果忘记了就可以去查。 实体映射文件中<hibernate-mapping>标签里的元素:auto-import元素设置自动导入包,默认是true,也就是默认是自动导入包。这样的好处是我们写HQL语句时直接写类名,不需要写包名。但是如果一个应用中有两个相同名的类,只是不同包,那么就会出问题,hibernate不知道查询那个类,所以这时可以加上包名就可以避免了。 package元素,指定实体类的包名,那么在<class>标签里映射实体类,只需写类名就可以了。 <class>标签完成实体类和表的映射,name属性指定实体类名,table属性指定表名,table可以不写,那么默认表名就用类名。 lazy属性,从hibernate3开始,全部默认是true,也就是默认支持懒加载。如果设置false,那么load方法就不会支持懒加载。 <id>标签,映射实体类的主键,映射单一主键。name属性表示实体类里主键标识名称。type属性指定hibernate自定义的类型,比如设置String,这个不是java的String类型,而是hibernate中的String类型,它的意思是将实体类中String类型映射到数据库表里的varchar或varchar2类型。详情见hibernate提供的中文文档节。如果不设置就采用默认类型,一般不需要设置,hibernate会自动根据实体类属性的类型映射到数据库字段的类型。但是时间类型还是需要设置的,因为数据库中的时间有好几种类型比如date(只有日期)、datetime(日期和时间)、timestamp(时间戳)类型,用hibernate的date、time、tiemstamp分别对应。 <property>标签是映射实体类普通属性的,它也有type属性,和上面讲的一样。也有column属性指定列名。 <id>和<property>标签都有access属性,默认值为property,表示hibernate采用set/get方法来访问实体类的属性值。也可以设成field,这样就是直接操作实体类的属性的值,不需要set方法。 主键生成器,在id标签里中间用<generator>标签表示,用class属性指定生成器类型。生成器类型有:increment自增:为主键生成唯一标识,标识类型为long、short、int,这是由hibernate生成的,不能在集群下不能使用,可能会重复。identity:DB2、MySQL、SQLServer、Sybase等数据库产品就是采用这种方式,生成标识类型为long、short、int。标识是由数据库产品生成的。sequence:Oracle等数据库采用这种方式,生成的标识类型是long、short、int,也是由数据库产品来生成的标识。还有hilo、seqhilo不常用。uuid:用128bit的UUID算法生成字符串类型的标识,这在一个网络中是唯一的(使用了IP地址)。UUID被编码为一个32位16进制数字的字符串。UUID是hibernate生成的,不需要访问数据库就可以生成UUID,这样效率就提供了。这也是常用的方式。native:根据底层数据库产品选择identity(mysql、db2、sqlserver)、sequence(oracle)、hilo中的一个作为主键生成器。也是由数据库生成assigned:让应用程序在sava()前味对象手动生成一个标识。如果没有写<generator>标签就采用这种方式。foreign:使用另外一个关联对象的标识拿过来,作为自己的标识。通常在一对一联合主键映射中使用。 <id>和<property>标签里都有length属性,表示生成字段的长度。注意:property只能映射普通属性。 <property>标签的update属性表示该字段是否参与更新语句,true表示参与,默认就是true。还有insert属性含义类似。 unique属性表示该字段的值必须唯一,默认是false,也就是允许重复。not-null表示该字段的值是否允许为null,true表示不能为null,false表示可以为null,默认是null。 注意:如果是oracle数据库,并且想一个表一个sequence,那么在映射文件里需要进一步配置,就不能简单的<generatorclass=”sequence”></generator>还需要进一步配置,给每一个sequence取给名:<generatorclass=”sequence”><paramname=”sequence”>seq_user_id</param></generator> 如果我们用native就不要这么麻烦了,直接配上就可以,它会根据底层数据库来自动创建。 注意:用hibernate的SchemaExport工具生成数据库表时,我们每一次调用都会先把数据库中原来的表删除,然后创建新表。我们可以用另一个属性,就可以不删除原来的表,并且创建新表(比原来新增的表)。如果一个表的字段修改了,那是不会更新的。//在hibernate.cfg.xml中配置<propertyname="hibernate.hbm2ddl.auto">update</property> 配置hbm2ddl的作用是:当我们创建sessionFacotry对象时,就负责创建数据库表(执行DDL语句)。我们设成update表示只创建新增的表,不会删除原来的表,也不会再次创建原来的表。如果我们再调用SchemaExport工具,照样是删除所有的表,再创建表。22讲多对一关联映射原理及具体映射 上面讲的映射是一个实体对象的映射,也是最简单的映射,实体对象之间没有任何关系。 下面我们讨论对象之间有关系的映射,比如用户和组两个实体类,对象之间关系是多对一的关联关系。 对象的关系是有方向性的,比如单向关联和双向关联。如果是单向关系,那么只能通过用户查询到组,不能通过组来查询用户。如果是双向关系就可以通过用户查询到组,也可以通过组查询到用户。但是数据库的关系模型是没有方向的。比如可以通过用户查询到组,也可以通过组查询到用户。 请看下面对象模型图: 箭头是用户指向组,也就是说只能通过用户查询到组,不能通过组查询到用户。 如果是聚合关系(强调是整体与部分的关系,可以分开存在),就用一个空菱形表示,对象模型如下: 实体对象和实体对象之间的关系就是用带箭头的线表示,这样表示通过用户查询到组对象。还有一种关系是组合关系,这是最强的关系,这是整体和部分不能分开,比如汽车和发动机对象,缺了发动机还叫汽车吗?发动机也没有独立存在的实际意义。 把上面对象模型映射成关系模型,关系模型图如下: 实体模型是通过用户对象可以查到组对象,所以在关系模型里,在用户表里肯定有个外键指向组的表里,这样才能通过用户查询到组。 也就是在多方加一个外键指向一方。这就是多对一的关联映射。这样的好处就是我们查询用户对象时,自动把组对象查询出来了。而且是发一条sql语句就完成了。 关联映射,就是将关联关系映射到数据库中,所谓的关联关系在对象模型中就是一个或多个引用。 多对一关联映射原来:在多的一端加入一个外键,指向一的一端。就是在User实体类中添加Group属性,并生成set/get方法privateGroupgroup;//生成set和get方法 User实体类映射文件:<hibernate-mapping> <classname="com.bjpowernode.hibernate.User"table="t_user"> <idname="id"> <generatorclass="native"/> </id> <propertyname="name"/>//映射关联属性(多对一关联) <many-to-onename="group"column="groupid"/> </class></hibernate-mapping> Group实体类映射文件:<hibernate-mapping> <classname="com.bjpowernode.hibernate.Group"table="t_group"> <idname="id"> <generatorclass="native"/> </id> <propertyname="name"/> </class></hibernate-mapping> group是sql的关键字,所以必须要重新命名。关联属性要用特殊标签映射,比如<many-to-one>标签就是映射多对一的关联关系。 <many-to-one>标签中name属性是关联属性的名称,就是把关联属性映射成数据库中的外键字段,默认会把外键名和关联属性名设成一样,这样因为group是sql关键字,所以必须用column元素重新命名外键的名称。 现在测试一下:session.beginTransaction();//创建组对象Groupgroup=newGroup();group.setName("动力节点");//创建用户对象Useruser1=newUser();user1.setName("张三");//设置关联属性user1.setGroup(group);Useruser2=newUser();user2.setName("李四");//设置关联属性user2.setGroup(group);//保存用户1和用户2session.save(user1);session.save(user2); 经过测试报异常:TransientObjectException,因为Group对象没有放到session里管理,它是暂态的。而User对象放到session管理是持久对象。因为我们映射文件里已经映射了关联属性,那么User对象就会找group对象的id来创建外键,而group是暂态的,User对象就找不到group对象(说白了是session找不到)。自然就报异常了。这个问题不注意经常会出现。 因为Group为Transient状态,没有被session,在数据库中没有匹配的数据,而User为Persistent状态,在清理缓存时hibernate在缓存中无法找到Group对象。得出结论是:Persistent(持久)状态的对象不能引用Transient(暂态)状态的对象。 正确的代码如下:session.beginTransaction();//创建group对象Groupgroup=newGroup();group.setName("动力节点");//保存group对象,这时就是持久对象session.save(group);//创建user1和user2对象Useruser1=newUser();user1.setName("张三");user1.setGroup(group);Useruser2=newUser();user2.setName("李四");user2.setGroup(group);session.save(user1);session.save(user2); 把group纳入session管理就变成了持久对象,和user对象都是持久对象,也就是这两个对象都已经映射到数据库表里了。这样就可以正确的为它们建立关联(在user表里创建外键引用group表的主键)。注意:上面操作的步骤,先手动调用save方法保存一方的对象,再调用save方法保存多方的对象。 级联特性cascade:就是保存某个对象时,顺便把它关联的对象也保存了。可以取值all、none、save-update、delete。默认是none,不使用级联。 all表示所有的操作都执行级联操作。save-update在保存和更新的时候执行级联操作。delete在删除的时候执行级联操作。 delete一般是不太合理的,删除用户就删除组,不太合乎逻辑。但是反过来就合理了,如果删除组,删除组下的所有用户,这是合理的,因为组没有了,用户就没有意义了。但是一般都不这么做,删除一般都不做级联。 下面是设置save-update级联,然后用上面出异常的代码重新测试,结果是正常的,不再报异常。<many-to-onename="group"column="groupid"cascade="save-update"/> 因为在调用save方法保存user对象时,因为设置了级联,它会自动保存它关联的对象group,所以就不会出异常。 上面测试是添加对象,下面测试查询对象:session.beginTransaction();Useruser=(User)session.load(User.class,3);System.out.println("="+user.getName());System.out.println("="+user.getGroup().getName());session.getTransaction().commit(); user.getGroup().getName()通过User对象导航到Group对象,然后查询Group对象的name属性。这段代码能正常执行。在发出查询User对象的sql语句后,又会发出一条sql语句去单独查询Group对象。 对于多对一来说,这种查询方式并不太好,因为为了查一个group对象,却单独发出了一条sql查询。我们可以做一些优化,用一个sql语句把user和group对象都查询出来。以后会讲的。 注意:级联特性只影响增删改,不影响查询操作。23讲回顾上午内容24讲一对一关联映射 一对一关联映射有两种:共享主键和唯一外键。 共享主键就是一个对象的主键标识引用另一个对象的主键标识。我们用人和身份证来说明:(其中一个实体类的主键标识同时也是外键标识,参照关联对象的主键标识) 根据Person对象来获取IdCard对象。所以在Person类里有IdCard的引用。 关系模型如下: 先演示单向关联:通过person对象可以查询到IdCard对象,单向是不能返过来查询的。就是在Person实体类中添加一个IdCard引用,并生成set和get方法。 联合主键的一对一关联映射原理:让两个实体的主键一样,这样就不需要加入多余的字段了。privateIdCardidCard;//生成set和get方法 IdCard实体类的映射文件:<hibernate-mapping> <classname="com.bjpowernode.hibernate.IdCard"table="t_idCard"> <idname="id"> <generatorclass="native"/> </id> <propertyname="cardNo"/> </class></hibernate-mapping> Person实体类的映射文件:<hibernate-mapping> <classname="com.bjpowernode.hibernate.Person"table="t_person"> <idname="id"> <!--采用foreign生成策略,forgeign会取得关联对象的标识--> <generatorclass="foreign"> <!--property只关联对象--> <paramname="property">idCard</param> </generator> </id> <propertyname="name"/> //name是关联属性名 <one-to-onename="idCard"constrained="true"/> </class></hibernate-mapping> 在Person实体映射文件中,主键生成策略采用foreign,它会获取关联对象的主键标识,作为自己的主键标识。并且用param指定从哪个关联对象中获取,name元素必须写property,然后把Person实体类中的关联属性名告诉它,idCard就是关联属性名称。 <one-to-one>标签是不会破坏表结构的,也就是不会对表添加字段的,不像<many-to-one>标签为表加一个外键字段。它的name属性就是表示关联属性名称。最好加上constrained属性,设置为true,表示当前的主键标识id同样也是外键,参照了IdCard的主键标识。也就是会生成外键约束语句。 <one-to-one>标签指示hibernate如何加载其关联对象,默认根据主键加载,也就是拿到关系字段值,根据对端的主键来加载关联对象。 测试保存对象代码:因为Person的主键参照了IdCard的主键,所以必须先创建IdCard。session.beginTransaction();//创建IdCard对象IdCardidCard=newIdCard();//创建Person对象Personperson=newPerson();person.setName("张三");//设置关联属性(建立关系)person.setIdCard(idCard);//保存person对象session.save(person);session.getTransaction().commit(); 执行结果是正常的,没有出异常。为什么我们没有配置级联特性,也没有调用save保存IdCard对象,没有出错呢?上午讲多对一的时候,如果没有配置级联也没有调用save方法保存Group对象,那么就会出异常。 这是因为hibernate在这种一对一关联采用共享主键的映射中,在保存Person对象前默认的把IdCard对象保存了,也就是说默认的实现了级联特性。所以不需要显示的保存IdCard对象,也不需要显示配置级联特性。 反过来只保存IdCard对象代码:session.beginTransaction();//创建IdCard对象IdCardidCard=newIdCard();1");//创建Person对象Personperson=newPerson();person.setName("张三");//设置关联属性(建立关系)person.setIdCard(idCard);//保存idCard对象session.save(idCard);session.getTransaction().commit(); 如果直接保存IdCard对象,数据库里IdCard是保存了,但数据库里没有保存Person数据。因为我们只建立了单向关联,关系的维护端在Person端,所以IdCard不知道Person是否存在,所以不能返过来保存。 测试查询代码:session.beginTransaction();Personperson=(Person)session.load(Person.class,1);System.out.println("="+person.getName());System.out.println("person.cardNo="+person.getIdCard().getCardNo());session.getTransaction().commit(); 可以把person对象查询出来,并且可以通过person对象查询IdCard对象。 一对一关联映射很少用共享主键的方式,灵活性差,常用唯一外键来实现。 一对一双向关联(还是以共享主键实现),也就是通过Person对象可以查询到IdCard对象,也可以通过IdCard对象查询到Person对象。双向关联的对象模型没有箭头,Person实体类有IdCard的引用,IdCard实体类里有Person的引用。 双向关联的关系模型还是没有变,和单向关联是一样的。 IdCard实体类映射文件:(没有变化,和单向关联时一样)<hibernate-mapping> <classname="com.bjpowernode.hibernate.IdCard"table="t_idCard"> <idname="id"> <generatorclass="native"/> </id> <propertyname="cardNo"/>//在IdCard映射文件中加上<one-to-one>标签映射关联属性 <one-to-onename="person"/> </class></hibernate-mapping> Person实体类映射文件:<hibernate-mapping> <classname="com.bjpowernode.hibernate.Person"table="t_person"> <idname="id"> <generatorclass="foreign"> <paramname="property">idCard</param> </generator> </id> <propertyname="name"/> <one-to-onename="idCard"constrained="true"/> </class></hibernate-mapping> 注意:<one-to-one>标签不会破坏数据库表结构,也就是不会为表增加外键字段。<many-to-one>标签就会为数据库表增加外键字段。 查询对象测试代码:session.beginTransaction();Personperson=(Person)session.load(Person.class,1);System.out.println("="+person.getName());System.out.println("person.cardNo="+person.getIdCard().getCardNo());session.getTransaction().commit(); 先查询IdCard对象,然后根据IdCard对象查询Person对象。执行结果是正常的。但是请仔细看控制台信息,只发出了一条sql语句,把IdCard和Person对象都查询出来了。这是跟抓取策略有关系的,以后会讲抓取策略。抓取策略说白了就是怎样加载关联对象,在<one-to-one>标签里用fetch属性配置,默认值是join。如果改成select,再次执行上面的代码,会发出两条sql语句,第一条sql语句查询IdCard对象,第二条sql语句查询Person对象。这个以后会讲。 <one-to-one>标签不影响存储,只影响加载对象。session.beginTransaction();//创建person对象Personperson=newPerson();person.setName("张三");//创建idCard对象IdCardidCard=newIdCard();//设置关联属性idCard.setPerson(person);//保存idCard对象session.save(idCard);session.getTransaction().commit(); 上面代码可以正常执行,没有异常,但是数据库里只保存了idCard的数据,没有保存person数据。因为我们先创建person对象,并没有保存,那么设置给idCard对象,等于没有设置,因为保存idCard的时候不知道是否和person有关系。所以关系仍然是person维护,所以保存对象时,还是要保存person对象,才可以级联的保存idCard对象。25讲一对一关联映射用唯一外键实现 单向一对一关联是采用多对一关联,并且把外键设置唯一约束,那么就是一对一单向关联了。这就是唯一外键实现方式。 对象模型还是上面一样的,关系模型如下: person实体映射文件:<hibernate-mapping> <classname="com.bjpowernode.hibernate.Person"table="t_person"> <idname="id"> <generatorclass="native"/> </id> <propertyname="name"/> <many-to-onename="idCard"unique="true"/> </class></hibernate-mapping> 采用了<many-to-one>就会创建外键。 保存对象测试代码:session.beginTransaction();IdCardidCard=newIdCard();Personperson=newPerson();person.setName("张三");person.setIdCard(idCard);session.save(person);session.getTransaction().commit(); 会抛出异常的,因为person依赖idCard对象,所以先要保存idCard对象,否则就会报异常。而且由于采用唯一外键所以hibernate没有默认提供级联特性,所以必须要手动保存idCard对象,才可以正确保存person对象。 所以在上面代码中,创建idCard对象后,手动调用save保存idCard对象,这样就不会出异常了。 双向一对一关联唯一外键实现:对象模型双向关联是没有箭头的,而且实体类里互相拥有对方的引用。关系模型还是和单向关联一样。 IdCard实体类的映射文件:<hibernate-mapping> <classname="com.bjpowernode.hibernate.IdCard"table="t_idCard"> <idname="id"> <generatorclass="native"/> </id> <propertyname="cardNo"/> <one-to-onename="person"property-ref="idCard"/> </class></hibernate-mapping> 用<one-to-one>标签映射关联属性person,property-ref属性指定外键对应的属性名(维护关系的属性名),不是数据库中外键名称,千万要小心。 查询对象的测试代码:可以通过IdCard对象查询Person对象。也是只发出一条sql语句把IdCard和person对象查询出来了。26讲flush的作用以及事务隔离级别 我们上面提到过清理缓存(脏数据比对),它是要生成sql语句的,hibernate和缓存结合很紧密,它要把缓存中的变化保存到数据库中,所以要生成sql语句。 hibernate什么时候清理缓存呢?在执行flush方法就会清理缓存。这个方法不需要手动调用,当我们提交事务时会自动调用flush方法。 flush方法做两件事:清理缓存,执行sql语句,但是不会提交事务。 session在什么情况下调用flush方法呢? 1提交事务时 2显示调用flush方法 3在执行查询前,比如iterate。 调用session的save方法保存对象时,并不是一定保存到数据库里,如果主键采用uuid生成器,它是不会去查询数据库的,是由hibernate生成的,当我们跟着save一个对象后,发现没有发出sql语句保存到数据库里。只能说这个对象被纳入了session管理,存到了session缓存里。当提交事务时,才真正发出sql语句保存到数据库里。其实真正起作用的是调用flush方法,提交事务时默认调用flush方法,执行sql语句保存到数据库里。 我们在save方法保存对象代码后面,手动调用flush方法,我们就能看到马上发出sql语句保存到数据库里。那为什么用数据库客户端查询数据,却没有数据呢?到底没有提交事务就没有保存吗?其实不是这样的,只要调用了flush方法执行了sql语句,就保存到了数据库。有没有提交事务只是影响可不可以回滚,不能影响保存数据库的。那为什么看不到呢?是因为数据库的隔离级别造成的,一般数据库产品的隔离级别是提交读或不可重复读,就是对于未提交事务的数据是不显示的,其实是插入到了数据库的。 事务有四个隔离级别:未提交读、提交读(oracle)、可重复读(mysql、sqlserver)、序列化(没有实际意义,严重影响并发性能)。 脏读:一个事务读取到了另一个事务未提交的数据,这个情况一定要避免。 幻读:幻读是针对添加数据的,一个事务读取到了另一个事务提交添加的数据。比如我们在一个页面查询22岁员工的数量,假设有5个人显示在页面中,这时另外一个事务对这个表添加了五条记录,如果我们刷新一下页面就变成10记录了。(幻读一般无法避免,它也不影响实际应用) 不可重复读:是针对现有数据的,一个事务前后两次读取的结果不一样,比如一个事务查询一个页面有记录张三、李四、王五,如果另一个事务把张三改成马六并提交事务,我们在页面中刷新一次,页面就变成马六、李四、王五了。如果用悲观锁就可以避免,不可重复读对业务没有太大影响,客户一般是可以接受的。 提交读级别可以避免脏读,可重复读级别可以避免脏读、不可重复读。序列化读级别都可以避免,全都锁住了,只有自己才能修改,这样也就没有时间一样。 提交读级别是最常用的。 测试native主键生成策略:测试跟踪发现,调用save保存一个对象时,马上发出sql语句,因为要到数据库中取对象的主键标识,所以需要马上发出sql语句查询。并且在没有提交事务前,我们把事务级别设成未提交读,对象已经保存在数据库里了。 注意:调用save方法保存一个对象后,是否马上保存到数据库里,得看情况,如果主键生成器采用数据库生成的,那么就保存到了数据库里。如果采用uuid等hibernate生成的,
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 2026年冷链物流箱保温性能测试行业报告
- 安全社区共建和谐发展-社区联动安全继续培训
- 麻纺厂气管检修细则
- 某污水处理厂员工防暑规范
- 2026年广东省阳江市单招职业适应性测试题库含答案详解(精练)
- 污水处理厂应急处置管理制度
- 2025至2030礼品包装行业消费习惯变迁及市场响应与私募融资研究报告
- 2026年广西城市职业大学单招职业适应性考试题库有答案详解
- 2026年广东松山职业技术学院单招职业技能测试题库带答案详解(a卷)
- 2026年广东省茂名市单招职业倾向性测试题库含答案详解(培优b卷)
- 近三年内未发生重大事故的安全生产承诺范本
- 岳阳职业技术学院单招职业技能测试参考试题库(含答案)
- 量子密码学与后量子密码学
- 部编版四年级下册语文写字表生字加拼音组词
- 威斯特年产10000吨纳米铜盐系列产品、6000吨叔丁基过氧化氢精馏及3000吨糊状过氧化二苯甲酰项目环境影响报告
- 广西-黄邵华-向量的数量积
- 1.2 国内外网络空间安全发展战略
- 2023年湖南省长沙县初中学生学科核心素养竞赛物理试题(含答案)
- 东北大学最优化方法全部课件
- 人教新课标六年级数学下册全册大单元教学设计(表格式)
- EBSD入门简介姚宗勇课件
评论
0/150
提交评论