首页 > 其他分享 >JDBC的使用与封装

JDBC的使用与封装

时间:2024-04-08 22:29:19浏览次数:15  
标签:JDBC 封装 使用 连接 获取 connection sql 我们 连接池

昨天学习了JDBC,连接数据库的操作,今天对JDBC做一个整合理解 

JDBC的简述:

JDBC是Java用来操作数据库的工具,实际就是不同的数据库实现了Java的接口,我们可以理解为:Java规范了接口,数据库实现了接口

作用:通过Java代码操作数据库

这里就是简述一下JDBC,如果大家如果想看的更详细,可以去看看大佬们的详述,我这边还是想直接敲代码带大家理解一下JDBC的使用,和通过反射来封装方法,使我们可以方便的插入和查询

JDBC的过程:

在敲代码之前,我们需要理解一下,我们是如何实现Java与数据库的连接(我这里的数据库是mysql )

通过这个图我觉得大家应该理解了java是如何对数据库进行操作的,我这里再详细解释一下

我们可以将整个过程看成是一个货车拉货的过程

我们知道java是面向对象的,所以我们需要将里面的每一个组件都变成对象

首先,我们需要知道我们要连接哪个数据库厂商,因为有mysql,mgdb等等数据库

第二步,我们需要创建一个连接通道,将java与数据库打通,我们的数据交流在该通道流动,这就是拉货的马路

第三步,我们需要获取一个货车,也就是执行者对象

第四步,我们要告诉货车,你要拉什么货,然后让他去拉货吧,也就是你需要执行什么sql语句,并开始执行

第五步,货车回来了,满载而归,取得了货物,也就是我们获取了我们执行sql语句后返回的数据

最后,我们需要把本次工作的东西全部拆除,卸磨杀驴,其实就是释放资源

JDBC的代码执行:

我们清楚了JDBC的执行过程,下面我们开始编写代码来实现,我们要先有一个数据库和表哈,我这里给大家准备了一个:

CREATE DATABASE IF NOT EXISTS test;
USE test;

CREATE TABLE IF NOT EXISTS t_user (
    id INT PRIMARY KEY AUTO_INCREMENT,
    account VARCHAR(20) NOT NULL UNIQUE COMMENT '账号',
    password VARCHAR(64) NOT NULL COMMENT '密码',
    nickname VARCHAR(20) NOT NULL COMMENT '昵称'
);

INSERT INTO t_user (account, password, nickname) VALUES ('root', '123456', '经理'), ('admin', '666666', '管理员');

 

哦对了,我们需要导入JDBC的jar包的,我不知道大家的IDEA的版本是多少,所以我这里就按我的举例了,首先我们要下载jar包,连接如下:

链接:JDBC的jar包
提取码:1111

我们首先需要在项目下创建一个lib文件夹,然后将两个jar包拖入

我这里截不了图,右键两个jar包,选择最下面有一个添加到库中,执行就好,出现可以展开,证明添加成功了,大家不懂可以去看看大佬们的方法

下面是Java中的执行过程,让我们牢记这6步:

1.注册驱动

//1.注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");

通过反射我们获取mysql的class对象,为什么执行一个获取class对象就能注册成功呢,我们需要知道,通过反射获取class对象时,会进行加载的过程,执行静态代码块,我们看一下Driver的源码

从源码可以看到,在静态代码块中实际上执行的,是new了一个Driver对象,所以我们可以获取到相应的对象

2.获取连接

Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/test", "root", "123456");

下面获取调用连接的方法,格式是这样的:

//jdbc:数据库厂商名://ip地址:prot/数据库名

//jdbc:mysql://127.0.0.1:3306/atguigu

3.创建执行者对象

Statement statement = connection.createStatement();

 我们通过连接来获取一个执行对象,也就是小车

4.执行sql语句

String sql = "select * from t_user;";
ResultSet resultSet = statement.executeQuery(sql);

sql语句中我们是查询了该表的所有信息,然后告诉卡车,你要拉什么货,就是你要查询的内容,然后返回查询结果

这里我说一下executeQuery,它接收的是查询语句 ,DQL

如果我们执行的是非DQL也就是增删改,我们需要使用executeUpdate这个方法

5.获取数据集并解析

while (resultSet.next()){
            int id = resultSet.getInt("id");
            String account = resultSet.getString("account");
            String password = resultSet.getString("password");
            String nickname = resultSet.getString("nickname");
            System.out.println(id + "--" + account + "--" + password + "--" + nickname);
        }

这里我觉得是和集合很像的,集合我们遍历是使用迭代器,然后利用迭代器的光标.next去获取集合的对象,这里的.next也是一样的,我们循环获取数据表的数据,get我就不多说了,字面的意思,数据库的字段类型是什么,那么这里就要获取对应的数据类型

6.关闭资源

resultSet.close();
statement.close();
connection.close();

最后我们释放资源,我们要记住我们创建了什么

一个通道,一个执行者(卡车),一个数据集(货物)

思考:

我们看这个过程实际上是有问题的

第一:不安全,会被sql注入,我们能不能规定,让我们自定义sql语句

第二:代码复用率太高了,我们不能执行一次数据就连接然后一顿操作,最后关闭,大货车来来回回跑,也会爆胎,效率太低,我们能不能让他只跑一次,装一堆数据回来

第三:连接时间的利用率低,我们将连接分为三个阶段,连接,使用,断开

如果连接+断开<使用,这样利用率高,如果连接+断开>>使用,利用率低,很显然我们属于第二种,所以有没有一种操作,可以让我们复用连接呢?

下面我们引入两个东西,连接池和自定义sql,也就是预编译

预编译:

预编译和上面的区别就是,我们需要先规定sql语句的格式,通过对占位符的赋值来进行操作,我们可以理解为传参,上代码:

import java.sql.*;

public class a {
    public static void main(String[] args) throws SQLException, ClassNotFoundException {
        //1.注册驱动
        Class.forName("com.mysql.cj.jdbc.Driver");

        //2.获取连接
        Connection connection = DriverManager
                .getConnection("jdbc:mysql://127.0.0.1:3306/test", "root", "123456");

        //3.创建statement
        Statement statement = connection.createStatement();

        //4.发送sql语句,获取返回结果
        String sql = "insert into t_user (account,password,nickname) values(?,?,?)";
        PreparedStatement preparedStatement = connection.prepareStatement(sql);
        //5.占位符赋值
        preparedStatement.setObject(1, "小红");
        preparedStatement.setString(2, "789");
        preparedStatement.setString(3, "服务员");
        //6.发送SQL语句
        preparedStatement.executeUpdate();

        //7.关闭资源
        preparedStatement.close();
        statement.close();
        connection.close();


        //6.关闭资源
        preparedStatement.close();
        statement.close();
        connection.close();

    }
}

 前面我不去说了,没有差距,只有一个地方有了变化,让我们看第四步,发送sql语句的部分

之前我们是写死的sql语句,现在我们改为占位符,我们需要记住这个api,prepareStatement

我们获取prepareStatement这个对象,将预编译的sql语句传入,下面我们要对占位符进行传参,数据库中是1开头的,而不是0,我们需要记住,然后给对应的问号赋值就好了

1,小红 ----》  account = 小红

2,789 ----》 password = 789

3,服务员 ---》 nickname = 服务员

然后让货车开走,executeUpdate,因为我们这次是非DQL语句,要用executeUpdate

连接池:

 我们需要知道连接池要干什么:

通常我们连接过后然后操作,最后释放掉,这个连接能不能提前获取,获取很多,我们需要的时候直接去拿,释放改成回收,让它重新回到池中

下面我不是JDBC的内容,说一下池的概念:

按我的理解,池和缓存其实是一样的,就是把东西先获取,然后存到内存里,需要的时候直接调用,还记得IO的缓存机制吗,不就是把数据统一放到数组中,将整个数组写入吗,按我的理解,池是相反的,我们先获取数据的整个数组,然后将数组里的数据,一个一个读取出来。

下面就是连接池的具体实现,我使用的连接池是Druid,对应的jar包在上面的链接里

在创建连接池之前我们需要知道properties配置文件

我这里不去讲硬编码的方式去创建连接池了,代码多而且灵活度非常低,所以使用properties配置文件,去读取配置文件的内容,然后进行连接创建,上代码:

druid.properties

driverClassName=com.mysql.cj.jdbc.Driver
username=root
password=123456
url=jdbc:mysql://127.0.0.1:3306/atguigu

切记切记,如果你是jdk版本和idea版本很高,一定要把配置文件放在src目录下,不要放在你的软件包下,不然会出错,本人血泪,这个问题卡了半天,还以为代码错误

JDBCutils类

package com.atguigu.api.utils;

import com.alibaba.druid.pool.DruidDataSourceFactory;

import javax.sql.DataSource;
import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;

/*内部包含一个连接池对象,并且对外提供获取连接和回收连接的方法
实现:
属性 连接池对象【实例化一次】
    单例模式
    static{
        静态代码块,全局调用一次
    }
方法:
 对外提供连接的方法
回收外部传入连接方法


利用线程本地变量,存储连接信息,确保一个线程的多个方法可以获取同一个connection
优势:事务操作的时候 service 和 dao 属于同一个线程,不用再传参数了
可以调用getConnection自动获取的是相同的连接池
*/
public class JdbcUtilsV2 {
    private static DataSource dataSource = null;

    private static ThreadLocal<Connection> threadLocal = new ThreadLocal<>();

    static {
        //初始化连接池对象

        InputStream ips = Thread.currentThread().getContextClassLoader().getResourceAsStream("druid.properties");

        Properties properties = new Properties();
        try {
            properties.load(ips);

        } catch (IOException e) {
            throw new RuntimeException(e);
        }

        try {
            dataSource = DruidDataSourceFactory.createDataSource(properties);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
    //对外提供获取连接的方法
    public static Connection getConnection() throws SQLException {
        //线程本地变量中是否存在
        Connection connection = threadLocal.get();
        //第一次没有
        if (connection == null) {
            //线程本地变量没有,连接池获取,返回本地变量的连接
            connection = dataSource.getConnection();
            threadLocal.set(connection);
        }

        return connection;
    }
    public static void freeConnection() throws SQLException {
        Connection connection = threadLocal.get();
        if (connection != null){
            threadLocal.remove();//清空线程本地变量数据
            connection.setAutoCommit(true);//事务状态回到默认状态
            connection.close();//回收到连接池
        }
    }
}

我这里是封装好的连接池,我给大家解释一下

我们需要知道怎么获取配置文件,首先创建配置文件对象

 Thread.currentThread().getContextClassLoader():获取当前线程的类加载器

getResourceAsStream() 是java中用于加载资源文件的方法之一。

它是 ClassLoader 和 Class 类提供的方法,用于从类路径(ClassPath)中获取资源文件并返回一个输入流 (InputStream

我们只需要知道它传入一个文件路径,返回的是一个输入流就好

Properties的 load()方法的用法是传入一个输入文件流的对象,然后load方法可以去读取里面的数据,也就是说可以把数据从文件中放到内存中展示。

这里就是获取配置文件的数据

 而连接池的创建非常简单,我们需要先获取DruidDataSourceFactory对象,然后将读取到的数据传入进行数据库连接就好,这时候连接池就已经创建好了

dataSource是一个接口,大家如果想了解很深,可以看看下面的文章

DataSource数据源_datasource数据源配置-CSDN博客

我在上面定义了静态变量来指向本次的连接,因为我们需要规定,我们要访问的是同一个连接对象

这是一个讲解线程本地变量的文章 

史上最全ThreadLocal 详解(一)-CSDN博客 

并定义了一个静态变量指向连接池,方便获取 

 这是一个提供连接的方法,首先我们判断线程本地变量是否存在,如果不存在,我们去池中获取,方法很简单:池对象.getConnection()返回一个连接

然后将该连接对象传入线程本地变量,最后返回此次的连接对象

这是一个释放方法,还是一个套路,首先获取线程本地变量是否存在,如果存在的话我们将其清空,然后回收该连接,并将事务状态变成true,如果没有开启事务也不会影响,主要是为了回归默认

连接池的SQL包装:

package com.atguigu.api.utils;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;

/*
 * 封装两个方法,一个简化增删改,非DQL
 * 一个简化查询,DQL*/
public class BaseDao {
    /*
     * 封装简化非DQL语句
     * sql 带占位符的sql语句
     * params 占位符的值
     * return 执行影响的行数*/
    public static int executeUpdate(String sql, Object... params) throws SQLException {
        Connection connection = JdbcUtilsV2.getConnection();
        PreparedStatement preparedStatement = connection.prepareStatement(sql);
        //可变参数可以当数组使用
        for (int i = 0; i < params.length; i++) {
            preparedStatement.setObject(i + 1, params[i]);
        }
        int rows = preparedStatement.executeUpdate();
        preparedStatement.close();
        if (connection.getAutoCommit()) {
            //没有开启事务,正常回收连接
            JdbcUtilsV2.freeConnection();
        }
        return rows;
    }

    public static <T> List<T> executeQuery(Class<T> clazz, String sql, Object... params) throws Exception {

        List<T> list = new ArrayList<>();
        //获取连接
        Connection connection = JdbcUtilsV2.getConnection();

        PreparedStatement preparedStatement = connection.prepareStatement(sql);

        for (int i = 0; i < params.length; i++) {
            preparedStatement.setObject(i + 1, params[i]);
        }
        //结果集解析
        ResultSet resultSet = preparedStatement.executeQuery();

        //获取列信息的对象
        ResultSetMetaData metaData = resultSet.getMetaData();
        //获取列信息的数量
        int columnCount = metaData.getColumnCount();
        while (resultSet.next()) {

            Constructor<T> declaredConstructor = clazz.getDeclaredConstructor();
            T t = declaredConstructor.newInstance();

            for (int i = 1; i <= columnCount; i++) {
                //列的属性值
                Object value = resultSet.getObject(i);
                //列的属性名
                String propertyName = metaData.getColumnLabel(i);
                //反射给对象的属性值赋值
                Field fileld = clazz.getDeclaredField(propertyName);
                fileld.setAccessible(true);
                fileld.set(t, value);
            }
            list.add(t);
        }
        resultSet.close();
        preparedStatement.close();
        if (connection.getAutoCommit()) {
            JdbcUtilsV2.freeConnection();
        }
        return list;
    }
}

我们在指向sql语句的时候,我们也是要写很多重复代码的,下面我将sql的执行部分,进行了封装,我来讲一下思路

非DQL语句的方法

我们想一下我们之前的sql执行

我们传入了两部分数据,一个是预编译的sql语句,一个是占位符的赋值

第一个参数很好理解,我们看第二部分,占位符的赋值

这一部分又被拆成两份,第一个是哪个占位符,第二个是对应占位符的数据

我觉得它很像一个数据结构,没错是数组

我们将占位符看成数组的索引,将传入的数据看成数组索引对应的数据,也就是说我们可以根据索引获取对应位置的值,那么我可以直接传入一个数组,就可以动态直接非DQL操作了

我们再看此方法,sql语句没什么好说的,Object... params这是java参数的特殊语法,相当于传入了一个数组,我们把params当初数组看就好,下面的操作就大差不差了,从连接池中获取连接对象,然后告诉货车你要拉什么货(sql参数),下面就是为对应占位符赋值了,那么直接使用循环动态获取,我们唯一要注意的是,占位符是从1开始的,所以赋值时候 i 要+1,数组不变,索引0是占位符1的数据,赋值结束,发车,将执行对象释放

最后判断一下该连接是否处于自动提交状态,也就是事务模式是否开启,如果没有开启,调用刚刚包装好的方法,进行回收。我需要解释一下为什么这里要加入一个判断开没开启事务,因为在整个业务中,我们需要业务层和控制层,而整个事务的提交,我们去统一的交给业务层去做

如果我们在这里就将本次操作的连接释放,那么下面的操作,就不再是相同的连接对象了

DQL查询的方法

我们先来想一下查询是不是获取的是行信息,而行信息,是由列信息组成的

我们转换到java的面向对象,我们是不是可以这样想,一行就是一个对象,而列信息就是该对象的属性,而整张表是不是一个集合,存放的都是相同的对象

 我的思路是,我不直接对数据表进行查询了,我构建一个表可以吧,也就是集合,我将所有对象也就是行数据,整合到一个集合中,最后返回这个集合,对该集合进行查询,不就可以了,而且我们是直接对内存进行操作,速度是非常快的,那我们开始实现一下

 这是本次包装的方法,参数比非DQL方法多了一个,我传入了一个Class对象,为什么,因为我们可以直接反射获取该对象的所有属性,并规定了集合存储的对象类型,这是泛型。

下面我创建一个存储该对象的集合,也就是本方法返回的内容

下面和非DQL的方法操作一样,我不多说了,给对应位置的占位符赋值,然后发车

 .getMetaData()是获取结果集,列的信息,大家可以看看下面这个文章,讲述了几种方法,我就不去讲了,直接说干什么了

java数据库编程(8)ResultSetMetaData_resultsetmetadata.getcolumncount-CSDN博客 

 metaData.getColumnCount()获取列的数量,也就是有多少属性值。

使用while循环遍历数据表,获取每一行的数据,我们的目的是将行数据变成对应的对象,存入到集合中,所以,利用反射获取其无参数构造函数,创建该行数据的对象

下面我们需要为该对象的属性赋值,我们通过metaData.getColumnCount()获取到列的数量也就是属性的数量,所以我们直接开始循环遍历,获取该行的每个列名和该列表对应的数据,并通过反射为此行对象进行属性赋值,最后将该行对象添加到集合中

示例

本次示例是通过封装的方法对数据库的两个用户进行转账功能,代码如下:

package com.atguigu.api.utils;

import org.junit.Test;

import java.sql.Connection;

public class BankService {
    @Test
    public void start() throws Exception {
        transfer("ergouzi","lvdandan",500);
    }
    public void transfer(String address, String subAddress, int money) throws Exception {

        BankDao bankDao = new BankDao();

        Connection connection = JdbcUtilsV2.getConnection();

        try {
            //开启事务
            //关闭事物提交
            connection.setAutoCommit(false);
            //执行数据库动作
            //执行数据库动作
            bankDao.add(address,money);
            System.out.println("==========");
            bankDao.sub(subAddress,money);
            bankDao.display();
            //事务提交
            connection.commit();
        }catch (Exception e) {
            //事物回滚
            connection.rollback();
            //抛出异常
            throw e;
        }finally {
            JdbcUtilsV2.freeConnection();
        }

    }
}

测试程序中,我执行了转账的这一个行为,传入三个参数,转账人,收款人,转账的金额,涉及到转账那么一定是一个事务,也就是转账人的金额减少,收款人的金额增加,如果不是一个事务的话,那么会出现一方没变一方增加的情况。

那么开始对数据库的操作,首先需要获取连接,调用封装的连接池,获取一个共用连接,关闭自动提交,开启事务执行增加金额和减少金额的方法,查询数据表,最后提交,如果事务失败,会进行回滚,事务结束后,进行回收。

下面我们看控制层

package com.atguigu.api.utils;

import java.util.List;

public class BankDao {
    /*
    * 加钱的数据库操作方法
    * account 加钱的行号
    * money 加钱的金额
    * */
    public void add(String account,int money) throws Exception {
        //3.编写SQL语句结果
        String sql = "update t_bank set money = money + ? where account = ?;";
        int i = BaseDao.executeUpdate(sql, money, account);
        System.out.println("加钱成功" + i);
    }
    /*
     * 减钱的数据库操作方法
     * account 减钱的行号
     * money 减钱的金额
     * */
    public void sub(String account,int money)throws Exception{

        //3.编写SQL语句结果
        String sql = "update t_bank set money = money - ? where account = ?;";
        int i = BaseDao.executeUpdate(sql, money, account);
        System.out.println("减钱成功" + i);
    }
    public void display() throws Exception {
        List<User> users = BaseDao.executeQuery(User.class, "select * from t_bank;");
        for (User user : users){
            System.out.println(user);
        }
    }
}
package com.atguigu.api.utils;

public class User {
    public int id;
    public String account;
    public long money;

    @Override
    public String toString() {
        return "User [id=" + id + ", account=" + account + ", money=" + money + "]";
    }
}

 

我们经过封装后,可以看到代码简洁了很多,下面就是安装封装好的调用就行,就用减钱的方法来说,传入减钱的用户,和金额,首先编写sql语句,然后将sql语句,用户,和金额传入到非DQL方法中,而DQL方法中,我们传入了行数据需要变成的对象,User类,然后传入sql语句,就会返回一个数据库表的集合,对集合进行遍历输出即可。

结果:

成功!

谢谢大家!如果有不对的地方希望大家指出! 

标签:JDBC,封装,使用,连接,获取,connection,sql,我们,连接池
From: https://blog.csdn.net/jiangyule/article/details/137325644

相关文章

  • 前端【Vuex】【使用介绍】
    1、组件下载vue与vuex的版本对应关系: Vue2匹配的Vuex3 Vue3匹配的Vuex4Vuex是一个专为Vue.js应用程序开发的状态管理模式+库。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。官方网......
  • 如何使用Docker容器化改善你的开发流程
    使用Docker容器化技术可以大大改善开发流程,提高软件开发和部署的效率。Docker提供了一个轻量级的、可执行的包装环境,使得应用程序可以在几乎任何地方以相同的方式运行,这减少了从开发到生产环境的“它在我的机器上可以正常工作”的问题。参考文档:如何使用Docker容器化改善你的开......
  • xshell安装和连接 bash shell 介绍和使用
     xshell安装和连接           在官网上注册一下可以选择学习来用的,是免费的但是差一些只有4个teble页 链接centos,将centos开启 在xshell中 找不到可以在文件夹里          #重启网卡systemctlrestartnetw......
  • Vue实现手机APP页面的切换,如何使用Vue Router进行路由管理呢?
    在Vue中,实现手机APP页面的切换,通常会使用VueRouter进行路由管理。VueRouter是Vue.js官方的路由管理器,它和Vue.js深度集成,使构建单页面应用变得易如反掌。以下是一个简单的步骤说明,展示如何使用VueRouter实现手机APP页面的切换:安装VueRouter如果你还没有安装VueRouter,可......
  • 在GitHub上用HTTP端口使用ssh
    问题:在实现本地仓库与GitHub仓库相关联时出现下图问题尝试了很多方法包括但不限于:更改公私钥密码,更改防火墙,检查仓库UPL等方法但都没有效果解决方法:通过HTTPS端口使用SSH测试有时,防火墙完全拒绝允许SSH连接。如果无法使用带有凭据缓存的HTTPS克隆,则可以尝试使用通过......
  • 为什么索引结构默认使用B+Tree?为什么需要注意联合索引中的顺序?最左前缀原则是什么?
    (1)为什么索引结构默认使用B+Tree,而不是B-Tree,Hash,二叉树,红黑树?B-tree:B+Tree相比于B-Tree,所有的数据都存储在叶子节点,并且叶子节点之间用指针相连形成了一个有序链表,这有利于范围查询和全表扫描时连续地读取磁盘上的数据,极大地降低了磁盘I/O次数。而在B-Tree中,数据分布在所有节......
  • 使用腾讯云Kubernetes部署SpringBoot项目
    使用流程创建集群创建Serverless类型的kubernetes集群(更加简单),不需要集群管理费用,但创建容器还是收费的。创建容器要确保当前账号有充足的余额在创建过程中,主要选择镜像,可以从自己的镜像仓库(需要先将自己的SpringBoot项目创建docker镜像并推送到远程仓库),或者Docker公共......
  • react中redux基本使用二
    1.action传参,用payload属性接收  <buttononClick={()=>dispatch(addNum(2))}>+2</button> 2.redux中异步操作,与同步类似,需要比同步多封装一个函数//使用RTK创建store,createSlice创建reducer的切片//使用RTK创建store,createSlice创建reducer的切片import......
  • Django框架之分页器使用
    一、问题描述针对上一篇章的批量插入数据,我们会发现一个很严重的问题,将所有数据都放到前端页面展示的时候一千多条数据放在了一页,这样太不方便,就像书本一样,不可能把所有内容都放在一页吧。所以我们可以也想书本一样,尝试做分页处理二、分页推导首先需要明确的是,get请求/post请......
  • 【SVN】windows SVN安装使用教程(服务器4.3.4版本/客户端1.11.0版本)
    原文地址:https://blog.csdn.net/weixin_60387745/article/details/130323395目录一、SVN的一些概念1、什么是SVN?2、SVN的作用是什么?为什么要用SVN?二、VisualSVNServer服务端–服务器搭建和使用1、官网下载VisualSVNServer服务端步骤:1、下载和搭建SVN服务器 2、建立用......