写在框架学习之前:请务必建立手动查文档的能力,主动学习
http://www.hibernate.org/downloads/
一:
1、导包:
hibernate4+oracle->window->preferences->java->build path->user libraries
build path->add libraries->user library->选择用户库
4.1以后的hbm2ddl配置update也能建表(不存在的情况下),只是不会显示建表语句,create无论什么时候都创建(有则删除),而且显示语句
2、配置:
hibernate.cfg.xml
a、从manual参考文档中copy或者从hibernate核心包中参考*.dtd
b、修改对应的数据库连接
c、注释掉用不上的内容
xml注释:ctrl+shift+c和ctrl+shift+/|ctrl+shift+
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
注意:hbm2ddl.auto
create:每次启动删除重建
update:启动检查实体类和表是否一致,没有重建,不一致修改表结构(不能删除列)
create-drop:创建工厂建表,关闭工厂删除
validate:启动验证实体类和表是否一致,不一致抛异常
3、创建实体类及映射文件(规范:类名和文件名相同,放在同一包下)
Student.hbm.xml Student.java (pojo:Plain Ordinary Java Object)
@Annotation选择javax.persistence包下的(jpa包)
a、从参考文档中copy或者从hibernate核心包中参考*.dtd
b、将映射文件配置到hibernate.cfg.xml
4、工具类HibernateUtil
4.0前的写法:只读取hibernate.cfg.xml
Configuration cfg = new Configuration().configure();
SessionFactory factory = cfg.buildSessionFactory();
Session session = factory.openSession();
4.0后的新写法:会读取hibernate.properties,hibernate.cfg.xml只能设置映射文件
Configuration cfg = new Configuration().configure();
ServiceRegistry service = new ServiceRegistryBuilder().applySettings(cfg.getProperties()).buildServiceRegistry();
SessionFactory factory = cfg.buildSessionFactory(service);
Session sess = factory.openSession();
注意:
1、configure()方法默认读的是classes目录下的hibernate.cfg.xml如果改了则要
configure('新文件')
hibernate.cfg.xml:show_sql、format_sql
2、Oracle表名和列名不支持_开头
3、文档中有帮助类HibernateUtil
annotation:@Entity @Table(name="表名") @Id @GeneratedValue @Column
hibernate可以自动识别java.sql.Date和java.util.Date
不需要持久化的字段:
xml:不配置,xml中配置了属性就必须存在(实际是去寻找get和set方法)
annotation:@Transient
*****************************************************************************************
二:
JUnit4的Bug:
@BeforeClass
public static void setUpBeforeClass() throws Exception {
//这里configure()出错不报异常,使用try catch或用main()方法解决
Configuration cfg = new AnnotationConfiguration().configure();
factory = cfg.buildSessionFactory();
}
openSession()方法和getCurrentSession()(ThreadLocal<T>)方法的区别:
openSession():始终打开的是新的session,必须要手动close(),查询可以不用事务(不用begin和commit)
getCurrentSession():他是从当前上下文环境中取一个session,如果第二次调用的时候他仍然会去取同一个,事务提交的时候自动close();不能手动close;
必须配置
<property name="current_session_context_class">thread</property>
Hibernate对象的三种状态:id=主键
瞬时状态transient: 对象只存在于内存中,缓存和数据库中都没有id
save() saveOrUpdate()将对象变为持久化状态
持久化状态persistent: 内存中有id,缓存中有id,数据库也有id
游离状态detached:(脱管)内存中有id,缓存中没有id,数据库有id sess.close()
核心开发接口
Configuration SessionFactory
session常用方法:
get():先检查缓存(session),没有该对象立马发sql,如果对象不存在返回null
load():始终返回代理对象($$),需要用到该对象的时候才检查缓存,没有才发sql,session关闭后再用到也将异常no session,get()较常用,load()不常用(可以使用Hibernate.initialize()加载)
update():必须数据库有对应记录(id)才更新,默认更新所有字段,持久状态更新前会检查有没有改变,没有不发sql,即使不用update只要提交事务也会检查session和数据库是否一致(脏检查),不一致也会发sql,dynamic-update(持久状态)不更新没有改动的字段
delete():删除,对象变成瞬时状态(只要对象的id在数据库存在就可以删除)
save():不管怎样都保存,即使有和数据库相同的id
saveOrUpdate():对象存在id(游离状态)则update,如果数据库有id则成功update没有对应的id则异常,对象没有id(瞬时状态)则save,通过这个方法可以将保存和更新两个业务合并
merge():合并类似saveOrUpdate,先查询出来(持久状态)和现有对象做比较,然后动态更新,如dynamic-update在跨session的时候就只能用merge()实现动态更新,如果瞬时状态也将做保存操作
注意:merge其返回类型是Object(始终持久状态)、saveOrUpdate无返回,
merge不改变参数对象的状态、saveOrUpdate将参数状态变为持久状态
clear():强制清空缓存,让对象变成游离状态
evict(obj):将对象实例从session缓存清除,该对象变为游离状态
flush():默认强制缓存和数据库同步一下,对象变为持久状态,但没有提交还可以回滚session没有关闭
session在commit的时候其实也flush了,也可以设置FlushMode(很少用)sess.setFlushMode()
Hibernate按照insert,update,delete的顺序提交所有登记的操作
只更新改变了值的字段:
1:dynamic-update
annotation:@DynamicUpdate(hibernate的包非jpa)
2:使用hql更新
*****************************************************************************************
三、
A、id生成策略:
xml查文档:
increment(使用max(id)函数查询再插入,不能用于集群)
native
sequence
identity
assigned:程序赋值
uuid|uuid2(Universally Unique Identifier|通用唯一标识符)
annotation查文档(JavaEE文档)4种:
1、AUTO(直接写 @GeneratedValue 相当如native)
i.默认:对 MySQL,使用auto_increment
ii.对 Oracle使用hibernate_sequence(名称固定)
2、IDENTITY(@GeneratedValue(strategy=GenerationType.IDENTITY))(sqlserver和mysql)
3、SEQUENCE(@GeneratedValue(strategy=GenerationType.SEQUENCE,generator="sss"))(oracle)
@SequenceGenerator(name="sss",sequenceName="tea_seq")
4、UUID:@GenericGenerator
@Id
@GeneratedValue(generator="system-uuid")
@GenericGenerator(name="system-uuid",stratege="uuid")
B、关联映射:对象间的关系,一共7种
查文档:
一对一:(少用,可以合成一张表,数据库表一般设计成单向外键关联或主键关联)
外键关联:(注意注解要么都写在属性上,要么都写在方法上)
单向:@OneToOne <many-to-one unique="true"/>
双向:@OneToOne(MappedBy,意思是由哪边维护关联关系,相当与xml中的property-ref)
<many-to-one unique="true"/> 两个都用<many-to-one/>也会生成两个外键
<one-to-one property_ref=""/>(property_ref相当于MappedBy,必须设置否则连表查询语句条件会有错)
注意:
1、对于双向关联除了双向主键关联,annotation必须加MappedBy(否则会产生冗余外键)
2、设置MappedBy还可以在添加(save)的时候由对方设置关联关系(setXxx)不再需要两边都设置
(特别特别重要)
一对多:数据库中是在多的一方加外键
单向:Set @OneToMany <One-To-Many />
多对多的特殊形式,默认有中间表,加@JoinColumn解决
多对一:
单向: @ManyToOne <Many-To-One /> 可以省略@JoinColumn不会产生中间表
双向: 默认产生两个外键字段
annotation:@OneToMany(mappedBy="")(只有@OneToMany有mappedBy @ManyToOne没有,也就是说由多的一方维护关系)
xml:<key>里的column必须和<many-to-one>的column保持一致
cascade="save-update|delete|all|none"
cascade=CascadeType.ALL|MERGE|PERSIST|REMOVE (用于merge|persist|delete方法)
建议:双向关联最佳应用就是在一的一方设置级联、inverse设置true,然后代码中设置好双向关联关系!如果在多一方设置级联而一的一方只设置了主键值则会清空除主键外的其他属性值
影响查询方式:
lazy="true|false|extra(加强延迟加载count)" fetch="select(多条)|subselect|join(连表)"
fetch=FetchType.LAZY|EAGER
多对多:(单向的比较重要)
单向:
<set name="roles" table="ur">
<key column="ur_uid" />
<many-to-many class="Role" column="ur_rid"></many-to-many>
</set>
双向:(双向的时候要设置inverse,否则关联对象如果两边设置关系会造成在中间表插入相同记录,将违反主键约束)
<set name="users" table="ur" inverse="true" >
<key>
<column name="ur_rid"></column>
</key>
<many-to-many class="Users" column="ur_uid"></many-to-many>
</set>
annotation:
@ManyToMany(如果不设MappedBy将产生两张中间表,两张表都将维护双方关系,而xml如果不设置inverse会在一张中间表中插入相同记录而违反主键约束(联合主键))
设置中间表和外键列:
@JoinTable(name="ur",joinColumns={@JoinColumn(name="ur_uid")},
inverseJoinColumns={@JoinColumn(name="ur_rid")})
关联对象的CRUD(cascade)
cascade:只影响增删改(多对多不影响删除),fetch影响查 (mappedBy对应xml中的inverse)
1、在@ManyToOne取多一方的时候默认会取一的一方(EAGER),而取一方的时候默认不取多(LAZY),我们一般采用默认就行,如果两边都使用eager会发多条sql语句,影响性能。
如:Wife2 w = (Wife2)sess.get(Wife2.class, 2);会发出两条sql
2、特殊情况下也有需要设置取一要取多的时候,例如树状结构,像论坛就不能取
3、不管设的是EAGER还是LAZY,手动都能取得数据,但是取数据的sql有点区别(了解)
4、测试各种单双向级联(all,merge,persist,remove)的update和delete,解释现象
5、注意merge,persist设置只在用merge和persist方法时有用
6、双向级联删除的时候默认会删掉相关两表的记录,可以设定关系为null或者写hql解决
7、通常在一的一方设置集合的级联操作,多的一方设置可能导致误操作(级联操作本质就是为了让关联对象保持状态一致)
8、inverse是配合级联操作的,通常代码设置好双向关联inverse就可以设置为true,避免多发update语句提高效率
注意包冲突:
部分annotation和load方法:和JavaEE5|JavaEE6包使用junit|main测试有冲突,可以使用JavaEE1.4
,不使用junit直接在web环境下使用没有冲突!
附加作业:树状结构
scott有emp表;empno ename mgr(empno)
写实体类映射emp表
*****************************************************************************************
四、数据查询语言 (和导航有关)
hibernate反向工程:myeclipse->project Capabilities->add Hibernate ...
1、添加hibernate包的时候选择myeclipse自带包
2、添加hibernate包的时候选择用户包
反向工程实现附加作业:树状结构
NativeSQL、HQL、QBC(Criteria)、QBE(Example)
HQL可用来做查询,更新和删除,但不能做添加,QBC、QBE只做查询
hql语句正确格式:(hql不可以用*)
from Husband | select name from Husband
select h from Husband h | select h.name from Husband h
错误格式:
select Husband from Husband | select Husband.name from Husband
Query query = sess.createQuery("from Husband where id<:id");
query.setParameter("id", 2); 用问号数字从0开始
链式:Query query = sess.createQuery("from Husband where id<:id").setParameter("id", 2);
封装条件对象:(注意这里只能使用命名参数不能使用?)
Qu q = new Qu();
q.setId(3);
Query query = sess.createQuery("from Husband h where id<:id").setProperties(q);
List<Husband> list = (List<Husband>)query.list();
其他实用重载设置参数方法:
setProperties(Map<String,Object>)
in查询或删除:
setParameterList("ids", ids(数组或集合));
分页:setMaxResults("每页显示几条")、setFirstResult("从这条记录开始但不包含这条")
返回唯一结果:uniqueResult() 必须是一条记录的结果集
long l = (Long)query.uniqueResult() (select count(0) from Wife) count返回long
from User与select id,name from User、select new Husband(id,name) from User区别:
返回类型不同:List<T> 和 List<Object[]>
内存管理不同:from出来的对象存在于session中,持久化状态,会与数据库同步
new Husband(id,name):必须是注册的pojo对象,游离状态,不会脏检查
连表查询:
查询通常和导航一起用:通过关联关系自动导航查询,没有关联关系的用手动导航join
Join查询:from Wife w join fetch w.husband h(只能关联属性) //而不能写 join Husband h
自定义类型查询:select new 类名(有参构造)(po) from Husband,同样这样查询出来的对象也不会与数据库同步,即使你把po的id属性也查询出来
属性是否为空:is null
集合是否为空:is empty | h.wifes.size=0
函数:lower(h.name)
其他关键字:in exist(in的效率低于exist)
对于 "from 类名" 找不到映射的两种情况:
1、同一工程有不同包同名实体类
解决:
a、xml修改其中<mapping/> auto-import="false"
b、注解使用@Entity(name="全路径")
2、使用了@Entity(name="表名")
解决:使用@Table(name="表名")
注意:项目中必须使用三层架构,事务可以使用过滤器实现
*****************************************************************************************
五、
一、HQL高级
类级别懒加载:<class lazy="true" .../> @Proxy(lazy=true)
load()也会立即加载,同时还影响关联关系中的lazy,比如many-to-one设置lazy=proxy仍然会加载一的一方,而且是通过一条内联接语句查询出来,但是对于one-to-many不会加载
distinct|max|sum|avg|count|min查询:
select distinct 属性名 from 类名 or select distinct(属性名) from 类名
fetch和非fetch的区别:
返回List<Wife>:from Wife w [left] join fetch w.husband
返回List<Object[]>:from Wife w [left] join w.husband
(Object[0]==Wife,Object[1]==Husband)
迫切联接和非迫切联接返回类型不同,但都是持久化状态
联接查询:
指定属性的表连接不能使用fetch,返回数据没有状态
返回List<Object[]>:select w.id,w.name,h.id,h.name from Wife w join w.husband h
隐式内连接查询(笛卡儿积,没有映射关联关系的情况):返回List<Object[]> 顺序同from后类顺序
from Husband h,Wife w where w.husband=h (不能h.wifes=w,集合和对象不能等)
from Husband h,Wife w where w.husband.hid=h.hid
from Husband h,Wife w where w.name=h.name
select w.wid,w.wname,w.husband.hname from wife w
二、性能优化:重点理解缓存问题
query.list()查询和query.iterate()查询的区别:
iterate:先取所有需要的id,然后用到哪个id就再根据id查询对应记录,第二次再iterate先看缓存是否存在,不存在才会才发sql,如果存在直接从缓存中取对象
list:二话不说直接取所有记录,即使同一事务中第二次list还会发sql去数据库取,不利用一级缓存(只会将查询结果放入缓存)
注意:<set...> 中设置了fetch="join"(联表查询)只对get()|load()|iterate()起作用(设置了join后,lazy属性将被忽略),但hql的list()方法将仍使用lazy属性
三、命名查询
@NamedQueries({@NamedQuery(name="",query="")})
@NamedNativeQueries({@NamedNativeQuery(name="",query="")})
xml的可以支持:(hql和sql都支持)
和class节点同级,可以调用存储过程
<query name="hh">
<![CDATA[
from Husband h where h.hid=:id
]]>
</query>
<sql-query name="husbandById">
select * from husband where id=1
</sql-query>
List<Object[]> list = sess.getNamedQuery("husbandById").list();
<sql-query name="hh">
<return class="com.cssl.pojo.Husband"/>
select * from husband where id=1
</sql-query>
SQLQuery query = (SQLQuery)session.getNamedQuery("hh");
List<Husband> list = query.list();
session.createSQLQuery("insert into youtable (username,password,email) values(?,?,?)");
session.createSQLQuery("select * from husband").addEntity(Husband.class).list();
session.createSQLQuery("select h.*,w.* from husband h inner join wife w on h.id=w.h_id");
query.addEntity("h",Husband.class).addEntity("w", Wife.class)|addJoin("w", "h.wifes");
注意:返回的结果集List<Object[]>中对象顺序和addEntity一致(别名也要保证顺序)
Object[0]:Husband,Object[1]:Wife
*****************************************************************************************
六、QBC、QBE
QBC:
Criteria、DetachedCriteria和Example
Criteria criteria = sess.createCriteria(Husband.class).add(Restrictions.idEq(1));
Map map = new HashMap(); map.put("id", 1); map.put("name", "admin1");
criteria.add(Restrictions.allEq(map)).ilike("name","A",MatchMode.EXACT);
OR:criteria.add(Restrictions.or(criterion1,criterion2,...));
criteria.add(Restrictions.or(Restrictions.idEq(1),Restrictions.or(Restrictions.idEq(3))));
criteria.add(Restrictions.disjunction().add().add().add()); //多条件or
连接查询:
sess.createCriteria(Husband.class,"h").createCriteria("wifes",JoinType.INNER_JOIN);
sess.createCriteria(Husband.class,"h").createAlias("wifes","w")
.add(Restrictions.eq("h.name","..."))
排序:criteria.addOrder(Order.asc("id")).addOrder(Order.desc("age"));
分页查询:criteria.setFirstResult(2).setMaxResults(3);
投影查询:
org.hibernate.criterion.Projections是 Projection 的实例工厂。
通过调用 setProjection()应用投影到一个查询。
criteria.setProjection(Projections.rowCount());
//查询指定属性
criteria.setProjection(Property.forName("hname"));
//多属性或多个投影条件查询必须使用
Projections.projectionList().add(Projections.count("wname")).add(Projections.groupProperty("husband"));
或
Projections.projectionList().add(Property.forName("hname").count()).add(Property.forName("").group());
QBE: ID(主键)不参与条件比较
Husband h = new Husband(); (这里的Example对象必须是pojo,不同于hql的参数对象)
h.setName("");
criteria.add(Example.create(h));
联接查询:使用了like,excludeZeroes()忽略数字类型属性值为0的条件
Husband hh = (Husband)sess.createCriteria(Husband.class).add(Example.create(h).ignoreCase().enableLike().excludeZeroes()).createCriteria("wifes").uniqueResult();
QBE不能使用范围,因为set方法只能赋一个值
QBE用于动态查询语句(如网页上有很多条件可选)的情况下比拼接hql要简单很多
使用 examples 在关联对象上放置条件:(一定要分开添加条件)
1:Criteria criteria = session.createCriteria(Wife.class);
2:criteria.add(Example.create(w).excludeZeroes()).createCriteria("husband").add(Example.create(h).excludeZeroes());
如果wife表还需要继续连表必须分开写:
3:criteria.createCriteria("type").add(Example.create(t));
如果接着写表示为Husband关联的对象而非Wife关联的对象
criteria.add(Example.create(w).excludeZeroes())
.createCriteria("husband").add(Example.create(hx).excludeZeroes())
.createCriteria("type").add(Example.create(t).excludeZeroes());
criteria.createCriteria("B")
criteria.createCriteria("C");
QBE仅适合于给特定值的查询
Example e = Example.create(tExample).ignoreCase().enableLike();
QBC除了添加自己的条件,同时也能将例子对象e也当成条件添加进来
QBC可以添加给特定值的条件,也可添加给了一定范围的条件
session.createCriteria(Topic.class).add(Restrictions.gt("id", 2)).add(e);
*****************************************************************************************
七:Hibernate的一级、二级和查询缓存
hibernate 1+N问题:(面试题)
查询多的一方默认会把一的一方查询出来(注解:Eager xml:lazy="false"),而且是发N条sql的方式查询
1、将多的一方设FetchType.LAZY,用到一的一方的时候才发sql语句,只是延迟而已
2、使用hql的迫切左外联接查询(left join fetch),只发一条(xml可以设置fetch="join"只对get|load方法有用)
3、注解可以使用Criteria查询,即使用hql的左外联接查询,只发一条
4、@BatchSize(size=?) 取一的一方的时候发1+N/?条sql | xml中的class标签中设置batch-size="5" 只有fetch="select"有用
一级缓存:Session级别
二级缓存:SessionFactory级别的(经常被访问、改动不大、数量有限、不是很重要允许出现偶尔并发的数据)
20.2. The Second Level Cache
引入ehcache的jar包(注意版本要和hibernate一致,否则异常)
配置hibernate.cfg.xml:
hibernate4.0在hibernate.cfg.xml配置二级缓存和hibernate3.x有所不同,这里的测试用load测试,用query的话就要配置查询缓存
4.0配置如下:
<property name="cache.use_second_level_cache">true</property>
<property name="cache.region.factory_class">org.hibernate.cache.ehcache.EhCacheRegionFactory</property>
3.x配置如下:
<property name="cache.use_second_level_cache">true</property>
<property name="cache.provider_class">org.hibernate.cache.EhCacheProvider</property>
复制etc下ehcache.xml
<defaultCache
maxElementsInMemory="10000" //最大元素
eternal="false" //永恒的
timeToIdleSeconds="1000" //发呆时间
timeToLiveSeconds="5000" //生存时间
overflowToDisk="true" //虚拟内存
/>
annotation:配置@Cache(usage=CacheConcurrencyStrategy.READ_WRITE)
|
<!— xml:
配置缓存,必须紧跟在class元素后面对缓存中的对象采用读写型的并发访问策略 -->
<cache usage="read-write"/>
<set name="products" table="products" cascade="all" inverse="true">
<!--
Hibernate只会缓存对象的简单属性的值,要缓存集合属性,必须在集合元素中也加入<cache>子元素,默认只缓存集合元素的ID,如果要缓存所有集合元素对象,必须在集合元素类型配置<cache...>,annotation同样在集合上及集合元素类型上加@Cache
-->
<cache usage="read-write"/>
......
</set>
也可以在hibernate.cfg.xml中配置哪些类的对象需要缓存:
<class-cache class="com.cssl.hibernate.Classes" usage="read-only" />
注意,这个<class-cache>标签必须放在<mapping>标签的后面!!
事务:ACID 原子性、持久性、隔离性、一致性
缓存同步策略:缓存同步策略决定了数据对象在缓存中的存取规则,我们必须为每个实体类指定相应的缓存同步策略.Hibernate中提供了4种不同的缓存同步策略:
1.read-only:只读.对于不会发生改变的数据可使用,缓存不更新,效率最高,事务隔离级别最低
2.nonstrict-read-write:缓存不定期更新,适用于变化频率低的数据
3.read-write:严格的读写缓存,实现了"read committed"事务隔离等级.缓存在数据变化时触发更新,适用于变化的数据
4.transactional:事务型缓存,必须运行在JTA事务环境中.缓存在数据变化时更新,并且支持事务,效率最低,事务隔离级别最高.
load()、get()、iterate()默认使用二级缓存,list()默认往二级缓存加数据,查询不用,除非打开查询缓存
查询缓存:
1、打开查询缓存要先打开二级缓存,然后代码还必须设置setCacheable(true)
2、只在查询语句一样、参数名一样、参数值一样的情况起作用
<!-- 打开查询缓存 -->
<property name="cache.use_query_cache">true</property>
sess.createQuery("from Husband h where h.hid<:id").setCacheable(true).list();
sess.createQuery("from Husband h where h.hid<:id").setCacheable(true).list();
查询缓存使用场合:
1、应用程序运行时经常使用的查询语句
2、很少对查询的数据进行增、删、改操作
如果查询结果中包含实体,查询缓存只会存放实体的OID,而属性查询会缓存所有数据值
其他性能优化:
1、大批量处理数据:在一个事务中处理大量数据
缺点:1、占用大量内存 2、执行大量的insert、delete或update操作频繁访问数据库
解决:
a、通过Session分批次执行,如每批次200条就flush()、clear()一次(commit()是提交,flush只是执行)
必须在hibernate.cfg.xml配置
<property name="jdbc.batch_size">200</property>
如果程序大于20也按20条提交
b、通过无状态StatelessSession,factory.openStatelessSession();
StatelessSession没有缓存,加载保存更新对象都没有状态,不与二级缓存交互,不进行任何级联操作
c、通过HQL或NativeSQL
2、集合过滤:多的一方数据庞大或需要对集合排序的情况,仍然要发sql语句
sess.createFilter(h.getWifes(), "where this.age>20 order by this.age");
3、使用容器中配置的数据源
由于hibernate默认的数据源不是专业的成熟的连接池产品,缺乏批量请求及容错能力
配置hibernate.cfg.xml:
<property name="connection.datasource">java:/comp/env/jdbc/MyDB</property>
从hibernate获取JDBC连接的方法:(调用存储过程)
4前的实现:
Session session=factory.openSession();
Connection con=session.connection();
4后替代方法:
Session session=factory.openSession();
session.dowork(new Work(){
@Override
public void execute(Connection conn) throws SQLException {
CallableStatement cs = connection.prepareCall("{call demo(?,?)}");
cs.setInt(1, 2);
cs.registerOutParameter(2, java.sql.Types.INTEGER);
cs.execute();
System.out.println(cs.getInt(2));
}
}
//DML也必须加事务
真正的高手并不是精通一切,而是精通在合适的场合使用合适的手段。