zoukankan      html  css  js  c++  java
  • hibernate sessionFactory

    hibernate中的SessionFactory,Session,configuration

    Session接口      
        Session接口对于Hibernate   开发人员来说是一个最重要的接口。然而在Hibernate中,实例化的Session是一个轻量级的类,创建和销毁它都不会占用很多资源。这在实际项目中确实很重要,因为在客户程序中,可能会不断地创建以及销毁Session对象,如果Session的开销太大,会给系统带来不良影响。但值得注意的是Session对象是非线程安全的,因此在你的设计中,最好是一个线程只创建一个Session对象。     

        在Hibernate的设计者的头脑中,他们将session看作介于数据连接与事务管理一种中间接口。我们可以将session想象成一个持久对象的缓冲区,Hibernate能检测到这些持久对象的改变,并及时刷新数据库。我们有时也称Session是一个持久层管理器,因为它包含这一些持久层相关的操作,诸如存储持久对象至数据库,以及从数据库从获得它们。请注意,Hibernate   session不同于JSP应用中的HttpSession。当我们使用session这个术语时,我们指的是Hibernate中的session,而我们以后会将HttpSesion对象称为用户session。   
        
        SessionFactory   接口   
        
        这里用到了一个设计模式――工厂模式,用户程序从工厂类SessionFactory中取得Session的实例。   
        
        令你感到奇怪的是SessionFactory并不是轻量级的!实际上它的设计者的意图是让它能在整个应用中共享。典型地来说,一个项目通常只需要一个SessionFactory就够了,但是当你的项目要操作多个数据库时,那你必须为每个数据库指定一个SessionFactory。   
      SessionFactoryHibernate中实际起到了一个缓冲区的作用,它缓冲了Hibernate自动生成的SQL语句和一些其它的映射数据,还缓冲了一些将来有可能重复利用的数据。   
        
        Configuration   接口   
        
        Configuration接口的作用是对Hibernate进行配置,以及对它进行启动。在Hibernate的启动过程中,Configuration类的实例首先定位映射文档的位置,读取这些配置,然后创建一个SessionFactory对象。   
        
        虽然Configuration接口在整个Hibernate项目中只扮演着一个很小的角色,但它是启动hibernate时你所遇到的每一个对象。   
        
        Transaction   接口   
        
        Transaction接口是一个可选的API,你可以选择不使用这个接口,取而代之的是Hibernate的设计者自己写的底层事务处理代码。   Transaction接口是对实际事务实现的一个抽象,这些实现包括JDBC的事务、JTA中的UserTransaction、甚至可以是CORBA事务。之所以这样设计是能让开发者能够使用一个统一事务的操作界面,使得自己的项目可以在不同的环境和容器之间方便地移值。   
        
        QueryCriteria接口   
        
        Query接口让你方便地对数据库及持久对象进行查询,它可以有两种表达方式:HQL语言或本地数据库的SQL语句。Query经常被用来绑定查询参数、限制查询记录数量,并最终执行查询操作。   
        
        Criteria接口与Query接口非常类似,它允许你创建并执行面向对象的标准化查询。   
        
        值得注意的是Query接口也是轻量级的,它不能在Session之外使用。   
        
        Callback   接口     
        
        当一些有用的事件发生时――例如持久对象的载入、存储、删除时,Callback接口会通知Hibernate去接收一个通知消息。一般而言,Callback接口在用户程序中并不是必须的,但你要在你的项目中创建审计日志时,你可能会用到它。  

     

    使用SessionFactory和Session:

    整个程序中只能有一个SessionFactory实例,每个线程中只能有一个Session(使用TreadLocal)
    SessionFactory里面有许多的Map,每个Map都存了许多的东西,占用内存较大

    注意Hibernate3中SessionFactory提供了一个getCurrentSession方法避免了线程间session的冲突问题。
    也就是执行语句:session=factory.getCurrentSession();

    注意要想执行上述语句的话必须首先在配置文件中的session-factory元素中加入:
    <property name="current_session_context_class">thread</property> 

    或者在属性文件中加入:hibernate.current_session_context_class thread

     

     

    配置文件和映射文件;SessionFactorySession的使用注意事项

     

    配置文件
    1 使用XML :
    调用Configuration.configure()方法,如果不传入参数的话,必须用默认名称hibernate.cfg.xml
    传入参数的话就可以用别的名称了

    2 使用properties文件:
    文件名只能是hibernate.properties,放在工程目录下,不用调用configure()方法,如果调用了的话,
    xml配置信息将会覆盖属性文件中配置好的信息。

    映射文件
    1 直接在hibernate.cfg.xml中,以mapping元素形式加入映射文件(如果用的是属性文件的话就没戏了)
    2 addClass,addFile,addResource方法导入映射文件,注意如果使用addClass的话,导入的是与类同名的、
    并以hbm.xml结尾的文件。

    --------------------
    使用SessionFactory和Session:

    整个程序中只能有一个SessionFactory实例,每个线程中只能有一个Session(使用TreadLocal)
    SessionFactory里面有许多的Map,每个Map都存了许多的东西,占用内存较大

    注意Hibernate3中SessionFactory提供了一个getCurrentSession方法避免了线程间session的冲突问题。
    也就是执行语句:session=factory.getCurrentSession();

    注意要想执行上述语句的话必须首先在配置文件中的session-factory元素中加入:
    <property name="current_session_context_class">thread</property> 
    或者在属性文件中加入:hibernate.current_session_context_class thread
    也是可以的。
    ---------------------
    1 知道如何在不使用默认文件名时,也能配置Hibernate。知道SessionFactory和Session在编程的时候需要注意什么问题?
    2 对象的三种状态分别是什么?含义?
    3 update方法和merge方法的区别,应用场合?
    4 saveOrUpdate方法应用场合?
    5 主键生成器什么意思?为什么实体一定要有标识符?如果数据表中没有主键的话,能否映射对象???
    6 HQL,Hibernate Query Language 要求必须非常熟练的掌握。

    -----------------------------
    一次会话状态中,持久化对象经历以下三种状态:
    1 transient:对象不与数据库中任意数据相关联。(也就是主键上面没有值)
    2 persistent:对象与数据库中的数据关联,包含持久标识,Hibernate保证对象持久标识与数据库主键一致。
    3 detached(脱管、游离状态)

    session = factory.getCurrentSession();
    session.beginTransaction();
    User u = new User();//u相对于session没有发生过任何关系,是临时状态的
    u.setId(3);        //现在u设置了主键,因此是脱管状态的(transient)

    session.save(u); //将与事件关联的对象缓存到与当前Session相对应的PersistenceContext对象中,
          //Session.flush()会生成一个flush()事件,从而写数据库。
          //save(u)也就是让u缓存到session中成为一个持久状态
    u.setUsername("a");
    session.getTransaction().commit();
    u.setPassword("bb"); //事务一旦commit,u已经不存在于session的缓存里面,这样再对u进行的操作就不会影响数据库了
           //这时u是脱管或是游离状态。所谓脱管就是和数据库没有关系了。
           //想让它再和session发生关系的话,就再创建一个session
    注意:如果使用的是getCurrentSession来创建session的话,在commit后,session就自动被关闭了,
    也就是不用再session.close()了。
    但是如果使用的是openSession方法创建的session的话,那么必须显示的关闭session,也就是
    调用session.close()方法。这样commit后,session并没有关闭,对象还是持久状态,而非脱管状态。
    除非调用了session.close(),才是脱管状态。

    session = factory.getCurrentSession();
    session.beginTransaction();
    session.saveOrUpdate(u);
    session.getTransaction().commit();

    //不论是脱管状态还是临时状态,save一定作插入,update则一定作更新!
    //使用saveOrUpdate方法的话,如果对象处于临时状态的话会进行插入,如果是脱管状态的话会更新。

    //如果我把一个脱管的对象u.setId(0)的话,由于id是主键,是整型,那么被置为0后就会被认为没有主键了,
    //既然没有了主键那就是临时状态,而不再是脱管状态了

    //就是说,如果对象里面的整型主键是0的话,且和session没有关系,那么就是临时状态,如果是非0值的话,
    //就是脱管状态!

    //在映射文件中的id元素中有一个属性叫做unsaved-value="",如果设置了是整数5的话,那么主键设置为5
    //会被默认为没有主键,调用saveOrUpdate方法的话肯定是去插入而不是去更新

    saveOrUpdate执行的伪代码:
    if(obj.getId()==unsavedValue)
    {
    save(obj);
    }
    else
    {
    update(obj);
    }

    session的save,persist,load,update,get等方法都是把临时状态转换为一个持久状态,将对象
    和session相关联。

    注意:在持久状态中,对对象的任何操作都会先写缓存,在commit后这些操作会反映到数据库。

    save无论对象是否处于临时状态都会向数据库中保存的,但是persisit只会去持久化那些处于临时状态中
    的对象(那些没有主键的对象,也就是被设置成unsaved-value值主键的对象)

    临时状态的最大特点是主键值与unsaved-value值相同!!
    ------------------------------------------
    代码片断1:

    //临时状态
        //最大特点是ID与unsaved-value的值相同。
        User u = new User();
       
       
        session = factory.openSession();
        session.beginTransaction();
       
        //持久状态
        /*
        * 因为u的ID值为0,而在映射文件中没有指定unsaved-value值,所以默认为0
        * 程序在执行saveOrUpdate时,会判断ID值是否与unsaved-value相同,如果
        * 相同则执行save
        */
        session.saveOrUpdate(u);
        u.setUsername("a");
       
        session.getTransaction().commit();

       注意:session.saveOrUpdate(u)这条语句://如果执行的是save则立刻执行,如果是update则到flush时才执行
    还有一点,session.save(Object)方法返回的值是主键的值!
    ----------------------------------------
    session.beginTransaction();
    session.get(User.class,new Integer(u.getId()));
    session.update(u); //注意上面那句话已经让session中缓存了对象u,如果再次进行update的操作的话
          //会出现错误!!如果不想错误发生的话,应该把update方法改为merge来进行更新!
    session.getTransaction.commit();

    ----------------------------------------
    代码片断2:
    //临时状态
        //最大特点是ID与unsaved-value的值相同。
        User u = new User();
       
        session = factory.openSession();
        session.beginTransaction();
       
        //持久状态
        /*
        * 因为u的ID值为5,而在映射文件中没有指定unsaved-value值,所以默认为0
        * 程序在执行saveOrUpdate时,会判断ID值是否与unsaved-value相同,如果
        * 不相同则执行update
        */
       
        session.saveOrUpdate(u);//如果执行的是save则立刻执行,如果是update则到flush时才执行
       
        u.setUsername("a");
       
        session.getTransaction().commit();
       
        //使用session.openSession在事务提交不会关闭session,
        //所以u处于持久状态(只有session关闭之后,u才会变为脱管状态)
        u.setPassword("bb");
        session.beginTransaction();
        session.getTransaction().commit();
        session.close();
       
        //脱管状态(游离)detached
        u.setUsername("cccc");
       
        session = factory.getCurrentSession();
        session.beginTransaction();
        session.get(User.class, new Integer(u.getId()));
        session.update(u); //这里如果不用merge的话是没有办法更新的,但是这里可以用save进行插入
        session.getTransaction().commit();
    ------------------------------------------------
    明天是张老师讲邮件服务器的细节,具体来说是怎么样用程序来实现收发邮件的问题。


    下午提前30分钟上课,不知为何……
    对象的三种状态分别是什么?含义?
    1 临时:没有和session发生关系的,没有主键的(默认情况下整型主键值为0,对象型主键值为null时)。
    2 脱管:和临时相比有了主键信息,也就是主键值不等于unsaved-value
    3 持久:和session相关对象。

    只有session里面没有缓存u时,才可以使用session.update(u)。如果session中有了一个对象和u的主键
    已经相同了,就不允许使用session的update方法了

    如果不确定这个session缓存里面是否有一个对象和u的主键有相同的值,那么使用merge来覆盖掉原来缓存
    中的对象,从而达到更新的目的。

    saveOrUpdate方法的应用场合:不确定记录是否要被插入到数据表中。
    什么时候执行save呢?
    对象主键的id等于unsaved-value的时候执行save。

    什么时候执行update呢?
    对象主键的id不等于unsaved-value的时候执行update。
    --------------------------
    再说说ThreadLocal,
    教室里面每个人都是一个线程,每个人的杯子都是一个ThreadLocal,前面有一杯水是session,还有一个
    水缸是sessionFactory,如果每个人都要取水的话都得在水缸前面排队,这就是线程安全。

    这样每一个线程都有了一个session,ThreadLocal是每个人手里的杯子,杯子里面都可以装水(session)。
    --------------------------
    主键生成器是什么意思??为什么实体一定要有标识符?
    没有主键的话还能不能映射对象了?

    主键生成器:<generator class="identity"/>
    在org.hibernate.id中包含IdentifierGenerator接口。
    可以用org.hibernate.id.IdentityGenerator替换上面的identity。

    其实identity是简写。

    主键是不用插入的,主键生成器是用来把插入记录生成的主键读出来放到对象里面的相应标识中去。
    注意:select last_insert_id();如果和插入语句放在同一个事务中,那么可以用它来得到最后一条
    插入记录的主键,如果它没有和插入语句放在同一个事务中,那执行起来就没有效果了。
    -------------------------------------

    下面讲HQL
    session = factory.getCurrentSession();//注意如果要想使用这句话的话必须
             //<property name="current_session_context_class">thread</property>
    session.beginTransaction();
    User u = (User)session.load(User.class,new Integer(3));
    //上面这种方式的加载如果不知道主键的话根本没有意义,如果我要想根据某些字段来查询表中的相应
    //记录集合的时候,用这种方法来加载是不行的
    //弊端:只能通过主键来加载对象,限制了Hibernate的应用

    //如果我想根据用户名和密码查询的话,HQL语句应该如何写呢?

    //如下所示,语句hql是从类里面找满足条件的对象,注意user是vo.User的别名,vo是类所在的包名。
    //如果我不想加入包名的话,那么就得在映射文件的hibernate-mapping元素中设置auto-import属性,
    //将之设置为true,就不用引入包了!
    String hql = "from vo.User user where user.username=? and user.password=? "
    Query q = session.createQuery(hql); //x需要导入org.Hibernate.Query包
    q.setString(0,"j2se");
    q.setString(1,"1");   
    List l = q.list();    //这句话也需要导入包!
    Iterator it = l.iterator();
    if(it.hasNext())
    {
    System.out.println("找到了!");
    }
    else
    {
    System.out.println("哎,没找到啊!");
    }

    session.getTransaction().commit();

    //上面iterator中存的就是对象!

    while(it.hasNext())
    {
    System.out.println(it.next().getClass());
    User u = (User)it.next();
    }
    -----------------------------------------------------------------
    使用Hibernate实现用户登陆和修改密码的小例子:


    package practice;

    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStreamReader;
    import java.util.Iterator;
    import java.util.List;

    import org.hibernate.Query;
    import org.hibernate.Session;
    import org.hibernate.SessionFactory;
    import org.hibernate.cfg.Configuration;

    public class Test {

    public static String readLine()
    {
       String result = "";
       BufferedReader in = new BufferedReader(
            new InputStreamReader(System.in));
       try {
        result = in.readLine();
       } catch (IOException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
       }
       return result ;
    }

    public static void main(String[] args) {

      
       System.out.println("请您输入登陆的用户名和密码!");
       System.out.println("×××××××××××××××××××××××××××××××");
       System.out.println("请您输入用户名:");
       String username = Test.readLine();
       System.out.println("请您输入密码:");
       String password = Test.readLine();
      
      
       Entity entity = new Entity();
      
       SessionFactory factory = 
        new Configuration().addClass(Entity.class).
         configure().buildSessionFactory();
       Session session = factory.getCurrentSession();
      
       session.beginTransaction();
      
       String hql = "from practice.Entity e where e.name=? and e.password=?";
      
       Query q = session.createQuery(hql);
       q.setString(0,username);
       q.setString(1,password);
      
       List list = q.list();
       Iterator it = list.iterator();
      
       if(it.hasNext())
       {
        System.out.println("您的输入正确!欢迎登陆");
        entity = (Entity)it.next();
       }
       else
       {
        System.out.println("输入的用户名、密码有误,下次小心点!");
        System.exit(1);
       }  
       session.getTransaction().commit();//很重要!在登陆完毕后就关闭session!后面用到的时候再创建!
      
       System.out.println("*************************************");
       System.out.println("请您输入您的新密码:");
       String password1 = Test.readLine();
       System.out.println("请您再次输入您的新密码:");
       String password2 = Test.readLine();
       if(!password1.equals(password2))
       {
        System.out.println("两次输入的密码不一致,下次小心点!");
        System.out.println("***************");
        System.exit(1);
       }
      
       entity.setPassword(password1); //这时候entity对象毫无疑问处于脱管状态!
      
       session = factory.getCurrentSession();
       session.beginTransaction();
       session.update(entity);//注意这里可以用saveOrUpdate和merge方法也可以达到同样的效果!
       System.out.println("密码已经成功的被更新!!!");
       session.getTransaction().commit();

    }
    }
    ---------------------------------
    再次注意下面的配置语句:
    <hibernate-mapping auto-import="true" package="vo">
    这里的auto-import是在hql语句中不用写包名了,那个package则是保证在下面的配置信息中的class的name属性
    中不必输入包名了!

    //如果不加select的话默认选取出的是整个对象,只有像下面这样写才能够取出具体的属性
    Query q = session.createQuery("select username from User where id=1");
    String name = (String)q.uniqueResult();
    System.out.println(name);
    -----------------------------------------

    <property name="name" column="name"/>
    其实里面还有一个叫做type的属性可以加上数据类型,这样指明了每个字段的数据类型的话运行起来会快一点,
    要不程序还得自己去根据反射机制去查找数据类型,挺麻烦的。

    mysql中的timestamp对插入操作会相应的把时间置为当前的时间。
    但是oracle中的时间戳完全不一样

    select systimestamp from dual;
    会得到09-12月-06 04.26.46.312000 下午 +08.00 ,也就是说oracle中包含了很详细的时间
    甚至有毫秒、时区等信息。

    date,time, timestamp,calendar,calendar_date
    string , varchar , 
    integer,long,short,float,double,byte,character,boolean,true_false,yes_no

    以上的这些类型都可以加入到<property name="name" column="name"/>
    的type属性中去。

    ---------------------
    总结:

    对象三种状态,update和merge区别,saveOrUpdate应用场合,unsaved-value,HQL语句掌握
    使用SessionFactory和Session注意的问题,

    当然还有一个今天没有太讲清楚的主键生成器!

    注意以下每行方法的对比、区别
    save、persist、saveOrUpdate
    update、merge、saveOrUpdate
    load、get
    load->setter、update

    练习:
    使用命令行方式实现用户注册,修改用户注册信息的功能,要求实现Hibernate实现。
    考虑性能问题,一定要注意及时的关闭session!

    明天是张老师讲JavaMail,后天是Hibernate中hql的qbe和qbc之类的东西,Hibernate进行完后
    是javaScript要讲一讲,然后是Javaweb、Struts、Sprint、设计模式、工作流之类的东东。

    ---------------------------------------------

    使用命令行方式实现用户注册,修改用户注册信息的功能,要求实现Hibernate实现。
    考虑性能问题,一定要注意及时的关闭session!

    晚上练习的答案:

    配置文件hibernate.cfg.xml:

    <?xml version="1.0" encoding="UTF-8"?>
    <!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="dialect">org.hibernate.dialect.MySQLDialect</property>
    <property name="connection.driver_class">com.mysql.jdbc.Driver</property>
    <property name="connection.url">jdbc:mysql:///j2ee</property>
    <property name="connection.username">root</property>
    <property name="connection.password">root</property>


    <property name="format_sql">root</property>
    <property name="current_session_context_class">thread</property>
    <property name="show_sql">false</property>

    <mapping resource="aw/User.xml"/>
    </session-factory>

    </hibernate-configuration>
    **********************************************

    映射文件User.xml:

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE hibernate-mapping PUBLIC 
        "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
        "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">

    <hibernate-mapping>
    <class name="aw.User" table="users">
       <id name="id">
        <generator class="identity"/>
       </id>
       <property name="name"/>
       <property name="password"/>
       <property name="loginTimes"/>
       <property name="loginTime"/>
    </class>

    </hibernate-mapping>
    **********************************************

    实体类User:

    package aw;

    import java.sql.Timestamp;

    public class User {
    private int loginTimes ;
    private Timestamp loginTime ;
    private String name ;
    private String password ;
    private int id ;
    public int getId() {
       return id;
    }
    public void setId(int id) {
       this.id = id;
    }
    public Timestamp getLoginTime() {
       return loginTime;
    }
    public void setLoginTime(Timestamp loginTime) {
       this.loginTime = loginTime;
    }
    public int getLoginTimes() {
       return loginTimes;
    }
    public void setLoginTimes(int loginTimes) {
       this.loginTimes = loginTimes;
    }
    public String getName() {
       return name;
    }
    public void setName(String name) {
       this.name = name;
    }
    public String getPassword() {
       return password;
    }
    public void setPassword(String password) {
       this.password = password;
    }


    }
    ×××××××××××××××××××××××××××××××××××××××××××××××××
    源程序:

    package aw;
    import java.util.Iterator;
    import java.util.List;

    import org.hibernate.Query;
    import org.hibernate.Session;
    import org.hibernate.SessionFactory;
    import org.hibernate.cfg.Configuration;
    import java.io.*;
    public class Aaa {

    //从控制台读一行后返回相应的字符串!
    public static String readLine()
    {
       try {
        return new BufferedReader(new InputStreamReader(System.in)).readLine();
       } catch (IOException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
        return "";
       }
    }

    //判断用户名是否已经存在
    public static boolean existsUser(SessionFactory factory,String username)
    {
       boolean result = false ;
       Session session = factory.getCurrentSession();
      
       session.beginTransaction();
      
       String hql = "from aw.User where name=?";
       Query q = session.createQuery(hql);
       q.setString(0,username);
       List list = q.list();
       Iterator it = list.iterator();
       if(it.hasNext())
        result = true ;
      
       session.getTransaction().commit();
      
       return result ;
      
    }




    //进行注册处理
    public static void register(User u, SessionFactory factory)
    {
       boolean flag = false ;
       System.out.println("现在开始注册:请您输入用户名");
       String username= readLine();
       if(existsUser(factory,username))
       {
        System.out.println("真是非常非常的抱歉,该用户名存在了!");
        flag = true ;
        register(u,factory);
       }
       //递规调用后必然要回到这里,如果执行了递规调用的话那么下面的代码没有执行
       //的意义,所以要根据flag的值决定是否返回入口点。
       if(flag==true)
        return ; //这句话非常的重要!不可丢失
       //下面是正常的注册逻辑!
       System.out.println("用户名输入成功,您再输入一下密码吧:");
       String password = readLine();
       //下面开始插入数据库!!
      
       u.setName(username);
       u.setPassword(password);
       Session session = factory.getCurrentSession();
       session.beginTransaction();
        session.save(u);
      
       System.out.println("您的注册已经完成了!");
       session.getTransaction().commit();
       return;
    }

    //修改密码处理:
    public static void modify(User u, SessionFactory factory)
    {
       System.out.println("请您输入新密码");
       String password = readLine();
      
       u.setPassword(password);
       Session session = factory.getCurrentSession();
       session.beginTransaction();
        session.update(u);
        System.out.println("密码修改完毕!!");
       session.getTransaction().commit();
    }

    public static void main(String[] args)
    {
       SessionFactory factory = new Configuration().configure()
             .buildSessionFactory();
       User u = new User();
       System.out.println("*****************************");
       register(u,factory);
       System.out.println("*****************************");
       System.out.println("请您输入新的密码:");
      
       modify(u,factory);
    }
    }

     

  • 相关阅读:
    iframe操作
    常用插件整理
    js原生的url操作函数,及使用方法。(附:下边还有jquery对url里的中文解码函数)
    espcms自定义表单邮件字段
    网页百度地图API相关资料
    css3 transition的各种ease效果
    点击按钮后延迟跳转
    js判断checkbox状态,处理表单提交事件
    js禁止网页使用右键
    jquery对url中的中文解码
  • 原文地址:https://www.cnblogs.com/weipeng/p/2417586.html
Copyright © 2011-2022 走看看