javaWeb项目结构

MySQL

注意sql sever和my sql的语法是不一样的

数据库的理解


结构化查询语言


DDL 数据库操作

SQL基本语法

在MySQL Command Line Client 或者navicat等工具中都可以编写SQL指令

  • SQL指令不区分大小写
  • 每条SQL表达式结束之后都以;结束
  • SQL关键字之间以空格进行分隔
  • SQL之间可以不限制换行(可以有空格的地方就可以有换行)

数据库的查询,创建,修改,删除

查询数据库

## 显示当前mysql中的数据库列表
show databases;## 显示指定名称的数据的创建的SQL指令
show create database <dbName>;

创建数据库

## 创建数据库  dbName表示创建的数据库名称,可以自定义
create database <dbName>;## 创建数据库,当指定名称的数据库不存在时执行创建
create database if not exists <dbName>;## 在创建数据库的同时指定数据库的字符集(字符集:数据存储在数据库中采用的编码格式 utf8  gbk)
create database <dbName> character set utf8;

修改数据库

修改数据库字符集

## 修改数据库的字符集
alter database <dbName> character set utf8;  # utf8  gbk

删除数据库 删除数据库时会删除当前数据库中所有的数据表以及数据表中的数据

## 删除数据库
drop database <dbName>;## 如果数据库存在则删除数据库
drop database is exists <dbName>;

使用/切换数据库

use <dbName>

基本表的创建,修改,删除

下面标注为黄色的为常用的,其他的不怎么使用


注意:
char与varchar的区别:char指的是长度不可变字符串,varchar指的是长度可变字符串
char后面的数字指的是最长的字符串长度,长度不够的话系统会自动补空格
而且varchar后面的数字也是指的最大长度,不过会根据实际的长度来使用,长度为4就用4个空间

创建数据表

数据表实际就是一个二维的表格,一个表格是由多列组成,表格中的每一类称之为表格的一个字段

create table students(stu_num char(8) not null unique,stu_name varchar(20) not null,stu_gender char(2) not null,stu_age int not null,stu_tel char(11) not null unique,stu_qq varchar(11) unique
);

查询数据表

show tables;

查询表结构

desc <tableName>;

删除数据表

## 删除数据表
drop table <tableName>;## 当数据表存在时删除数据表
drop table if  exists <tableName>;

修改数据表

## 修改表名
alter table <tableName> rename to <newTableName>;## 数据表也是有字符集的,默认字符集和数据库一致
alter table <tableName> character set utf8;## 添加列(字段)
alter table <tableName> add <columnName> varchar(200);## 修改列(字段)的列表和类型
alter table <tableName> change <oldColumnName> <newCloumnName> <type>;## 只修改列(字段)类型
alter table <tableName> modify <columnName> <newType>;## 删除列(字段)
alter table stus drop <columnName>;

删除

主键约束


数据的查询,插入,修改,删除


查询




聚合函数



日期函数


字符串函数


分组查询

这里与sql server的区别就是分组后不聚合依旧会显示第一条数据

分页查询

插入

注意插入数据时intto后面的列名可以不用按表中的顺序出现,但是value的值一定要与into后面的列名相对应,但如果你省略了列名,这需要按表中的列名的顺序一样

修改

删除

索引的建立和删除

删除

数据表的关联关系

一对一关联


一对多关联

多对多关联

外键约束



对上面添加外键约束的一些理解:首先constraint 是关键字,FK…是逻辑名称方便后面进行删除可以随便起名
然后是创建表的顺序首先是创建没有外键约束的表可以插入数据,然后创建有约束的表并建立关联,最后插入数据

级联



上面是添加外键时不添加级联的方法,很麻烦,一般不使用

连接查询,内连接,左 右外连接

内连接(只获取条件匹配成功的数据)



使用on后他不会直接生成笛卡尔积,他会判断连接条件是否成立先,如果不使用on的话会直接先生成笛卡尔积

左右外连接

嵌套查询, 子查询


UNION可以将多个查询语句的结果整合在一起

  1. 只要两个结果集的列数相同就可以使用,即使字段类型不相同,也可以使用。
  2. 列数相同,但字段类型不同。如goods_name是字符类型而goods_id是整形,但同样可以union。值得注意的是union后字段的名称以第一条SQL为准


注意这里与上面的区别,因为上面的只返回了一个值(单列单行),所以可以使用=来进行判断

存储过程

sql指令的执行过程


存储过程的执行过程


存储过程的创建及调用,变量使用,参数,分支if

创建存储过程

调用存储过程


注意上面的dual时什么?dual其实是一张系统表,我们定义的变量都会保存到这个表中

存储过程中变量的使用

局部变量

用户变量

变量赋值


存储过程的参数

存储过程的流程控制


循环语句

存储过程管理

因为我们使用了native图形界面所以可以很简单的管理我们的存储过程,可如果只使用sql语句又该如何管理存储过程?

查询存储过程

修改存储过程

删除存储过程

使用存储过程实现借书案例

数据准备

-- 创建数据库
create database db_test3;-- 使用数据库
use db_test3;-- 创建图书信息表:
create table books(book_id int primary key auto_increment,book_name varchar(50) not null,book_author varchar(20) not null,book_price decimal(10,2) not null,book_stock int not null,book_desc varchar(200)
);-- 添加图书信息
insert into books(book_name,book_author,book_price,book_stock,book_desc)
values('Java程序设计','亮亮',38.80,12,'亮亮老师带你学Java');
insert into books(book_name,book_author,book_price,book_stock,book_desc)
values('Java王者之路','威哥',44.40,9,'千锋威哥,Java王者领路人');-- 创建学生信息表
create table students(stu_num char(4) primary key,stu_name varchar(20) not null,stu_gender char(2) not null,stu_age int not null
);-- 添加学生信息
insert into students(stu_num,stu_name,stu_gender,stu_age) values('1001','张三','男',20);
insert into students(stu_num,stu_name,stu_gender,stu_age) values('1002','李四','女',20);
insert into students(stu_num,stu_name,stu_gender,stu_age) values('1003','王五','男',20);

业务分析

-- 实现借书业务:
-- 参数1: a 输入参数  学号
-- 参数2: b 输入参数  图书编号
-- 参数3: m 输入参数  借书的数量
-- 参数4: state 输出参数  借书的状态(1 借书成功,2 学号不存在,3 图书不存在, 4 库存不足)
create procedure proc_borrow_book(IN a char(4),IN b int, IN m int,OUT state int)
begindeclare stu_count int default 0;declare book_count int default 0;declare stock int default 0;-- 判断学号是否存在:根据参数 a 到学生信息表查询是否有stu_num=a的记录select count(stu_num) INTO stu_count from students where stu_num=a;if stu_count>0 then-- 学号存在-- 判断图书ID是否存在:根据参数b 查询图书记录总数select count(book_id) INTO book_count from books where book_id=b;if book_count >0 then-- 图书存在-- 判断图书库存是否充足:查询当前图书库存,然后和参数m进行比较select book_stock INTO stock from books where book_id=b;if stock >= m then-- 执行借书-- 操作1:在借书记录表中添加记录insert into records(snum,bid,borrow_num,is_return,borrow_date) values(a,b,m,0,sysdate());-- 操作2:修改图书库存update books set book_stock=stock-m where book_id=b;-- 借书成功set state=1;else-- 库存不足set state=4;end if;               else-- 图书不存在set state = 3;end if;else-- 不存在set state = 2;end if;
end;-- 调用存储过程借书
set @state=0;
call proc_borrow_book('1001',1,2,@state);
select @state from dual;

游标

事务


MySQL事务管理

自动提交与手动提交


如何保证一致性?因为sql在数据库的默认形势下,一旦sql执行成功就会自动将结果提交到数据库,就例如上图第一个语句执行成功后就会自动提交,但是语句却有可能因为错误而导致语句执行失败,假如这两个语句时有联系的操作,那么这种情况就破坏了事务的一致性


事务隔离级别



设置数据库的隔离级别


隔离级别就是上面表格中的级别,系统默认为可重复读

数据库设计

数据库设计的三范式



数据库建模

PowerDesigner

PowerDesigner的作用当我们建完模生成物理模型后,能自动帮我们生成sql语句,但我们一般不使用,因为生成的代码不够符合规范,气死

PDMan

JDBC

JDBC介绍


入门案例




JDBC实现增删改查

insert操作

删除操作

修改操作

查询操作


JDBC核心类与接口

DriverManager类

Connection接口

Statement接口


ResultSet接口

解决sql注入问题

例子如下:

删除操作

添加操作

修改操作

根据ID查询图书信息

工具类的封装

接口类的封装

package com.qfedu.jdbc.utils;import java.sql.*;public class DBHelper {/*将创建数据库连接所需的字符串定义为常量,集中管理*/private static final String DRIVER = "com.mysql.cj.jdbc.Driver";private static final String URL = "jdbc:mysql://localhost:3306/db_test3?characterEncoding=utf8";private static final String USERNAME = "root";private static final String PASSWORD = "@QFedu123";/*** 注册驱动只需执行一次,因此我们放在帮助类的静态初始化块中完成*/static{try {Class.forName(DRIVER);} catch (ClassNotFoundException e) {System.out.println("-----------注册驱动失败");}}/*** 创建数据库连接对象*/public static Connection getConnection(){Connection connection = null;try {connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);} catch (SQLException e) {System.out.println("-----------创建连接失败");}return connection;}/*** 关闭连接* 多态的应用:使用Statement接口做参数,既可以传递Statement接口对象,* 也可以传递PreparedStatement接口对象*/public static void close(Statement statement, Connection connection){close(null,statement,connection);}/*** 关闭连接*/public static void close(ResultSet resultSet,Statement statement, Connection connection){try {if(resultSet!=null && !resultSet.isClosed()){resultSet.close();}if(statement!=null && !statement.isClosed()){statement.close();}if(connection!=null && !connection.isClosed()){connection.close();}}catch (Exception e){System.out.println("~~~~~关闭数据库连接失败");}}}

DAO与DTO的封装


例子如下:

实体类DTO封装

public class Book implements Serializable {private int bookId;private String bookName;private String bookAuthor;private double bookPrice;private int bookStock;private String bookDesc;@Overridepublic String toString() {return "Book{" +"bookId=" + bookId +", bookName='" + bookName + '\'' +", bookAuthor='" + bookAuthor + '\'' +", bookPrice=" + bookPrice +", bookStock=" + bookStock +", bookDesc='" + bookDesc + '\'' +'}';}public Book() {}public Book(int bookId, String bookName, String bookAuthor, double bookPrice, int bookStock, String bookDesc) {this.bookId = bookId;this.bookName = bookName;this.bookAuthor = bookAuthor;this.bookPrice = bookPrice;this.bookStock = bookStock;this.bookDesc = bookDesc;}//get和set方法
}

使用实体类封装查询操作返回的结果:

/**
* 根据图书ID查询一条图书记录
*/
public Book queryBook(int bid) throws SQLException{Book book = null;Connection connection = DBHelper.getConnection();String sql = "select book_id,book_name,book_author,book_price,book_stock,book_desc from books where book_id=?";PreparedStatement preparedStatement = connection.prepareStatement(sql);preparedStatement.setInt(1,bid);//通过executeQuery方法执行查询语句,并且将查询的结果存放到一个ResultSet对象中(结果集)ResultSet rs = preparedStatement.executeQuery();//处理结果:从rs中获取查询结果if(rs.next()){int id = rs.getInt("book_id");String name = rs.getString("book_name");String author = rs.getString("book_author");double price = rs.getDouble("book_price");int stock = rs.getInt("book_stock");String desc = rs.getString("book_desc");//我们需要将查询到的一条数据库图书记录的6个值返回book = new Book(id,name,author,price,stock,desc);}//关闭连接  结果集也需要关闭DBHelper.close(rs,preparedStatement,connection);return book;
}

使用DTO封装查询结果

package com.qfedu.jdbc.les1;import com.qfedu.jdbc.dto.Book;
import com.qfedu.jdbc.utils.DBHelper;import java.sql.*;
import java.util.ArrayList;
import java.util.List;/*** @Description* @Author Java涛哥  @ 千锋教育* @千锋Java微信公众号 Java架构栈*/
public class TestSelectBooks {public static void main(String[] args) throws ClassNotFoundException, SQLException {List<Book> list = new TestSelectBooks().listBooks();for(Book b:list){System.out.println(b.getBookName()+"\t"+b.getBookAuthor());}}public List<Book> listBooks() throws SQLException{List<Book> bookList = new ArrayList<>();//查询所有图书信息Connection connection = DBHelper.getConnection();String sql = "select book_id,book_name,book_author,book_price,book_stock,book_desc from books";Statement statement = connection.createStatement();ResultSet rs = statement.executeQuery(sql);while(rs.next()){int bookId = rs.getInt("book_id");String bookName = rs.getString("book_name");String bookAuthor = rs.getString("book_author");double bookPrice = rs.getDouble("book_price");int bookStock = rs.getInt("book_stock");String bookDesc = rs.getString("book_desc");Book book = new Book(bookId, bookName, bookAuthor, bookPrice, bookStock, bookDesc);bookList.add(book);}DBHelper.close(rs,statement,connection);return bookList;}}

实体类传递添加、修改操作参数

添加操作
public boolean insertBook(Book book) throws SQLException {boolean flag = false;//调用工具类,获取数据库连接对象Connection connection = DBHelper.getConnection();String sql = "insert into books(book_name,book_author,book_price,book_stock,book_desc) values(?,?,?,?,?)";PreparedStatement preparedStatement = connection.prepareStatement(sql);preparedStatement.setString(1,book.getBookName());preparedStatement.setString(2,book.getBookAuthor());preparedStatement.setDouble(3,book.getBookPrice());preparedStatement.setInt(4,book.getBookStock());preparedStatement.setString(5,book.getBookDesc());int i = preparedStatement.executeUpdate();  flag = i>0;//关闭连接DBHelper.close(preparedStatement,connection);return flag;
}
修改操作
 public boolean updateBook(Book book) throws SQLException{boolean flag = false;Connection connection = DBHelper.getConnection();String sql = "update books set book_name=?,book_author=?,book_price=?,book_stock=?,book_desc=? where book_id=?";PreparedStatement preparedStatement = connection.prepareStatement(sql);preparedStatement.setString(1,book.getBookName());preparedStatement.setString(2,book.getBookAuthor());preparedStatement.setDouble(3,book.getBookPrice());preparedStatement.setInt(4,book.getBookStock());preparedStatement.setString(5,book.getBookDesc());preparedStatement.setInt(6,book.getBookId());int i = preparedStatement.executeUpdate();flag = i>0;DBHelper.close(preparedStatement,connection);return flag;}

DAO的封装

import java.util.ArrayList;
import java.util.List;/*** @Descript DAO Data Access Object 数据访问对象* @Author 千锋涛哥* 公众号: Java架构栈*/
public class BookDAO {public boolean deleteBook(int bid) throws SQLException {boolean flag = false;//使用JDBC,根据图书编号删除图书信息//1.注册驱动 创建连接Connection connection = DBHelper.getConnection();//3.编写SQLString sql = "delete from books where book_id=?";//4.如果SQL指令有参数占位符?,则从Connection获取PreparedStatement预编译SQL指令//  预编译:在SQL指令中的参数赋值之前对SQL执行的语法结构进行编译PreparedStatement preparedStatement = connection.prepareStatement(sql);//  SQL指令预编译之后,给SQL中的?赋值preparedStatement.setInt(1,bid);//5.执行SQLint i = preparedStatement.executeUpdate();//6.处理结果flag = i>0;//7.关闭连接DBHelper.close(preparedStatement,connection);return true;}/*** 添加图书* @return 如果添加成功返回true,如果添加失败则返回false* @throws SQLException*/public boolean insertBook(Book book) throws SQLException {boolean flag = false;//调用工具类,获取数据库连接对象Connection connection = DBHelper.getConnection();String sql = "insert into books(book_name,book_author,book_price,book_stock,book_desc) values(?,?,?,?,?)";PreparedStatement preparedStatement = connection.prepareStatement(sql);preparedStatement.setString(1,book.getBookName());preparedStatement.setString(2,book.getBookAuthor());preparedStatement.setDouble(3,book.getBookPrice());preparedStatement.setInt(4,book.getBookStock());preparedStatement.setString(5,book.getBookDesc());int i = preparedStatement.executeUpdate();  // 如果i>0,表示DML操作是成功的;如果i=0表示DML操作对数据表中的数据没有影响flag = i>0;//关闭连接DBHelper.close(preparedStatement,connection);return flag;}/*** 根据图书ID查询一条图书记录*/public Book queryBook(int bid) throws SQLException{Book book = null;Connection connection = DBHelper.getConnection();String sql = "select book_id,book_name,book_author,book_price,book_stock,book_desc from books where book_id=?";PreparedStatement preparedStatement = connection.prepareStatement(sql);preparedStatement.setInt(1,bid);//通过executeQuery方法执行查询语句,并且将查询的结果存放到一个ResultSet对象中(结果集)ResultSet rs = preparedStatement.executeQuery();//处理结果:从rs中获取查询结果if(rs.next()){int id = rs.getInt("book_id");String name = rs.getString("book_name");String author = rs.getString("book_author");double price = rs.getDouble("book_price");int stock = rs.getInt("book_stock");String desc = rs.getString("book_desc");//我们需要将查询到的一条数据库图书记录的6个值返回book = new Book(id,name,author,price,stock,desc);}//关闭连接  结果集也需要关闭DBHelper.close(rs,preparedStatement,connection);return book;}public List<Book> listBooks() throws SQLException{List<Book> bookList = new ArrayList<>();//查询所有图书信息Connection connection = DBHelper.getConnection();String sql = "select book_id,book_name,book_author,book_price,book_stock,book_desc from books";Statement statement = connection.createStatement();ResultSet rs = statement.executeQuery(sql);while(rs.next()){int bookId = rs.getInt("book_id");String bookName = rs.getString("book_name");String bookAuthor = rs.getString("book_author");double bookPrice = rs.getDouble("book_price");int bookStock = rs.getInt("book_stock");String bookDesc = rs.getString("book_desc");Book book = new Book(bookId, bookName, bookAuthor, bookPrice, bookStock, bookDesc);bookList.add(book);}DBHelper.close(rs,statement,connection);return bookList;}public boolean updateBook(Book book) throws SQLException{boolean flag = false;Connection connection = DBHelper.getConnection();String sql = "update books set book_name=?,book_author=?,book_price=?,book_stock=?,book_desc=? where book_id=?";PreparedStatement preparedStatement = connection.prepareStatement(sql);preparedStatement.setString(1,book.getBookName());preparedStatement.setString(2,book.getBookAuthor());preparedStatement.setDouble(3,book.getBookPrice());preparedStatement.setInt(4,book.getBookStock());preparedStatement.setString(5,book.getBookDesc());preparedStatement.setInt(6,book.getBookId());int i = preparedStatement.executeUpdate();flag = i>0;DBHelper.close(preparedStatement,connection);return flag;}}
DAO类中代码的优化

1.在应用程序开发中,如果方法中抛出异常且自己可以处理,则直接通过try/catch进行捕获处理;

2.JDBC操作方法的连接需要放在finally中进行关闭;

3.将数据库连接Connection、Statement、ResultSet等需要关闭的数据库对象定义在try之前;

4.因为所有的JDBC操作都需要Conection、Statement对象,查询方法都需要ResultSet对象,因此在DAO中可以将这些对象定义成类的成员变量

package com.qfedu.jdbc.dao;import com.qfedu.jdbc.dto.Book;
import com.qfedu.jdbc.utils.DBHelper;import java.sql.*;
import java.util.ArrayList;
import java.util.List;/*** @Descript DAO Data Access Object 数据访问对象* @Author 千锋涛哥* 公众号: Java架构栈*/
public class BookDAO {private Connection connection;private Statement statement;private PreparedStatement preparedStatement;private ResultSet rs;public boolean deleteBook(int bid) {boolean flag = false;try{connection = DBHelper.getConnection();String sql = "delete from books where book_id=?";preparedStatement = connection.prepareStatement(sql);preparedStatement.setInt(1,bid);int i = preparedStatement.executeUpdate();flag = i>0;}catch(SQLException e){e.printStackTrace();}finally {DBHelper.close(preparedStatement,connection);}return flag;}public boolean insertBook(Book book) {boolean flag = false;try{//调用工具类,获取数据库连接对象connection = DBHelper.getConnection();String sql = "insert into books(book_name,book_author,book_price,book_stock,book_desc) values(?,?,?,?,?)";preparedStatement = connection.prepareStatement(sql);preparedStatement.setString(1,book.getBookName());preparedStatement.setString(2,book.getBookAuthor());preparedStatement.setDouble(3,book.getBookPrice());preparedStatement.setInt(4,book.getBookStock());preparedStatement.setString(5,book.getBookDesc());int i = preparedStatement.executeUpdate();  // 如果i>0,表示DML操作是成功的;如果i=0表示DML操作对数据表中的数据没有影响flag = i>0;}catch (SQLException e){e.printStackTrace();}finally {DBHelper.close(preparedStatement,connection);}return flag;}public Book queryBook(int bid) {Book book = null;try {connection = DBHelper.getConnection();String sql = "select book_id,book_name,book_author,book_price,book_stock,book_desc from books where book_id=?";preparedStatement = connection.prepareStatement(sql);preparedStatement.setInt(1, bid);//通过executeQuery方法执行查询语句,并且将查询的结果存放到一个ResultSet对象中(结果集)rs = preparedStatement.executeQuery();//处理结果:从rs中获取查询结果if (rs.next()) {int id = rs.getInt("book_id");String name = rs.getString("book_name");String author = rs.getString("book_author");double price = rs.getDouble("book_price");int stock = rs.getInt("book_stock");String desc = rs.getString("book_desc");//我们需要将查询到的一条数据库图书记录的6个值返回book = new Book(id, name, author, price, stock, desc);}}catch (SQLException e){e.printStackTrace();} finally {DBHelper.close(rs, preparedStatement, connection);}return book;}public List<Book> listBooks(){List<Book> bookList = new ArrayList<>();try {connection = DBHelper.getConnection();String sql = "select book_id,book_name,book_author,book_price,book_stock,book_desc from books";statement = connection.createStatement();rs = statement.executeQuery(sql);while (rs.next()) {int bookId = rs.getInt("book_id");String bookName = rs.getString("book_name");String bookAuthor = rs.getString("book_author");double bookPrice = rs.getDouble("book_price");int bookStock = rs.getInt("book_stock");String bookDesc = rs.getString("book_desc");Book book = new Book(bookId, bookName, bookAuthor, bookPrice, bookStock, bookDesc);bookList.add(book);}}catch (SQLException e){e.printStackTrace();}finally {DBHelper.close(rs, statement, connection);}return bookList;}public boolean updateBook(Book book)  {boolean flag = false;try {connection = DBHelper.getConnection();String sql = "update books set book_name=?,book_author=?,book_price=?,book_stock=?,book_desc=? where book_id=?";preparedStatement = connection.prepareStatement(sql);preparedStatement.setString(1, book.getBookName());preparedStatement.setString(2, book.getBookAuthor());preparedStatement.setDouble(3, book.getBookPrice());preparedStatement.setInt(4, book.getBookStock());preparedStatement.setString(5, book.getBookDesc());preparedStatement.setInt(6, book.getBookId());int i = preparedStatement.executeUpdate();flag = i > 0;}catch (SQLException e){e.printStackTrace();} finally {DBHelper.close(preparedStatement, connection);}return flag;}}

JDBC综合案例



测试DAO中的方法

junit的使用

package com.qfedu.jdbc.test;import com.qfedu.jdbc.dao.StudentDAO;
import com.qfedu.jdbc.dto.Student;
import org.junit.Test;import java.util.List;import static org.junit.Assert.*;/*** @Descript 此类是StudentDAO的单元测试类* @Author 千锋涛哥* 公众号: Java架构栈*/
public class StudentDAOTest {//1.在测试类中定义成员变量:创建被测试类的对象private StudentDAO studentDAO = new StudentDAO();//2创建测试方法 : 用来测试StudentDAO类中的insertStudent方法//a.测试方法名=test+被测试方法名//b.测试方法无参数无返回值@Testpublic void testInsertStudent(){//准备被测试方法所需的参数Student stu = new Student("1008","Tom","男",20);//调用被测试方法,获取结果boolean b = studentDAO.insertStudent(stu);//断言返回结果(成立 | 不成立)assertTrue(b);}@Testpublic void testQueryStudent(){String snum = "1008";Student student = studentDAO.queryStudent(snum);assertEquals("Tom2",student.getStuName());}@Testpublic void testListStudents(){List<Student> studentList = studentDAO.listStudents();assertEquals(8,studentList.size());}
}

JDBC事务管理

步骤如下

  1. 一个事务中的多个DML操作必须基于同一个数据库连接

  1. 创建连接之后关闭自动提交


3. 完成所有的操作后提交事务

4. 在出现异常后能够进行回滚,一般将回滚操作放到catch中

例子如下:

package com.qfedu.jdbc.dao;import com.qfedu.jdbc.utils.DBHelper;import java.sql.*;/*** @Descript* @Author 千锋涛哥* 公众号: Java架构栈*/
public class BookDAO {/*** 借书:* @param stuNum 借书的学生学号* @param bookId 借书的图书编号* @param num   借书的数量* @return*/public boolean borrowBook(String stuNum,int bookId,int num){boolean flag = false;Connection connection = null;PreparedStatement preparedStatement1 = null;PreparedStatement preparedStatement2 = null;try {connection = DBHelper.getConnectin();//设置使用当前Connection连接操作数据库自动提交关闭connection.setAutoCommit(false);//1.向`records`表添加借书记录String sql1 = "insert into records(snum,bid,borrow_num,is_return,borrow_date) values(?,?,?,0,sysdate())";preparedStatement1 = connection.prepareStatement(sql1);preparedStatement1.setString(1,stuNum);preparedStatement1.setInt(2,bookId);preparedStatement1.setInt(3,num);int i = preparedStatement1.executeUpdate();int k = 10/0;  //造异常:算术异常//2.修改`books`表中的库存String sql2 = "update books set book_stock=book_stock-? where book_id=?";preparedStatement2 = connection.prepareStatement(sql2);preparedStatement2.setInt(1,num);preparedStatement2.setInt(2,bookId);int j = preparedStatement2.executeUpdate();// 提交事务connection.commit();flag = i>0 && j>0;}catch (Exception e){try {//一旦事务执行过程中出现异常,执行回滚connection.rollback();} catch (SQLException ex) {ex.printStackTrace();}}finally {DBHelper.close(preparedStatement1,null);DBHelper.close(preparedStatement2,connection);}return flag;}}

service层的事务管理

service层介绍

问题就是在serveces层中如果想要实现事务管理就必须保证,它调用的dao操作中的连接时一致的,可是显然在不同的dao中获取到的连接时不同的怎么班?
方法1:直接先在serves中获取连接,然后将该链接发送到相关的dao操作中去(这种方法用的相对较少)

为什么不常使用:

方法2:使用ThreadLocal容器,实现多个DML操作使用相同的连接

对DBHelper代码的进一步改进


我们可以使用list集合来保存connection,通过list来确保他们获取的是同一个连接
但有一点需要注意的是我们在关闭连接是我们必须要在整个事务都完成后再关闭连接,不能像之前那样完成一个dao操作就关闭连接,因为后面的dao操作可能还需要使用该连接

使用ThreadLocal进一步改进DBHelper

但使用list作为容器在多线程并发执行的操作下,会报错因为如果操作的是同一个的连接,当其中一个线程将连接关闭会,可能会使其他的线程报错

ThreadLocal容器的作用是:对于一个线程会为其创建一个键值对,就类似于map,所以对于一个线程来说第一次就是创建连接,第二次就是获取之前创建的连接,所以使用ThreadLocal就可以解决多线程并发带来的问题



整个例子:

注意因为使用了ThreadLocal来保存连接,所以在关闭连接的时候也需要获取到相应的连接然后再关闭,而且关闭后需要将其从容器中移除

  package com.qfedu.jdbc.utils;import java.sql.*;import java.util.ArrayList;import java.util.List;/*** @Descript* @Author 千锋涛哥* 公众号: Java架构栈*/public class DBHelper {//1.定义数据库连接信息private static final String DRIVER = "com.mysql.cj.jdbc.Driver";private static final String URL = "jdbc:mysql://localhost:3306/db_test3?characterEncoding=utf8";private static final String USERNAME = "root";private static final String PASSWORD = "@QFedu123";//1️⃣定义ThreadLocal容器private static final ThreadLocal<Connection> local = new ThreadLocal<>();//2.静态初始化块注册驱动static{try {Class.forName(DRIVER);} catch (ClassNotFoundException e) {e.printStackTrace();}}//3.创建数据库连接public static Connection getConnectin(){// 2️⃣从ThreadLocal容器中获取连接Connection connection = local.get();try {if(connection == null){//3️⃣如果容器中没有连接,则创建连接,并将创建的连接存放到容器connection = DriverManager.getConnection(URL,USERNAME,PASSWORD);local.set(connection);}} catch (SQLException e) {e.printStackTrace();}return connection;}//4. 4️⃣关闭连接// 如果使用ThreadLocal存储数据库连接,关闭连接时同时要将Connection对象从ThreadLocal中移除public static void closeConnection(){// 获取到当前线程使用的数据库连接对象Connection connection = local.get();try {if(connection !=null && !connection.isClosed()){connection.close();}// 将关闭后的连接对象从ThreadLocal中移除local.remove();} catch (SQLException e) {e.printStackTrace();}}public static void closeStatement(Statement statement){closeStatement(null,statement);}public static void closeStatement(ResultSet resultSet, Statement statement){try {if(resultSet!=null && !resultSet.isClosed()){resultSet.close();}if(statement!=null && !statement.isClosed()){statement.close();}} catch (SQLException e) {e.printStackTrace();}}}

数据库连接池

什么是数据库连接池


我们现在的DAO操作中每一个操作都会申请连接,以及完成操作会回家给连接进行销毁,即使使用了ThreadLocal也会在不同的线程拆功能键不同的连接,所以就会产生资源的浪费,所以就有了数据库连接池

常用的数据库连接池有哪几种


目前市面上使用最多的是druid

使用数据库连接池

  package com.qfedu.jdbc.utils;import com.alibaba.druid.pool.DruidDataSource;import com.alibaba.druid.pool.DruidDataSourceFactory;import java.io.IOException;import java.io.InputStream;import java.sql.Connection;import java.sql.SQLException;import java.util.Properties;/*** @Descript Druid数据库连接池工具类* @Author 千锋涛哥* 公众号: Java架构栈*/public class DruidUtils {//1.定义DruidDataSource对象:表示Druid数据库连接池(数据源)private static DruidDataSource druidDataSource;//2.静态代码块初始化定义DruidDataSource对象static{try {//读取druid.properties文件中配置的属性InputStream is = DruidUtils.class.getResourceAsStream("druid.properties");Properties properties = new Properties();properties.load(is);//使用属性文件初始化DruidDataSource对象druidDataSource = (DruidDataSource) DruidDataSourceFactory.createDataSource(properties);} catch (IOException e) {e.printStackTrace();} catch (Exception e) {e.printStackTrace();}}//3.创建静态方法,从连接池对象中获取连接public static Connection getConnection(){Connection connection = null;try {connection =  druidDataSource.getConnection();} catch (SQLException e) {e.printStackTrace();}return connection;}}

通用的JDBC操作封装

DML的操作封装

具体代码如下:
增加,删除,修改的sql语句都是通过excuteUpdate来执行的,所以可以封装在一起

package com.qfedu.jdbc.utils;import java.sql.Connection;
import java.sql.PreparedStatement;/*** @Descript 公共DAO,用于封装公共的JDBC操作* @Author 千锋涛哥* 公众号: Java架构栈*/
public class CommonDAO {/*** 公共DML操作* @param sql 执行的SQL指令* @return*//*** sql : delete from students where stu_num=?* args: snum** sql : insert into students(stu_num,stu_name,stu_gender,stu_age) values(?,?,?,?)* args: 1009  Lucy  女   18*/public boolean update(String sql, Object... args){boolean b = false;try{Connection connection = DruidUtils.getConnection();PreparedStatement preparedStatement = connection.prepareStatement(sql);for (int i = 0; i < args.length ; i++) {preparedStatement.setObject(i+1,args[i]);}int i = preparedStatement.executeUpdate();b = i>0;}catch (Exception e){e.printStackTrace();}return b;}}

DML的操作封装

因为在不同的数据库中的数据类型可能不一样,有可能是student类型,也可能是book类型,也就是说不同的查询中的返回类型可能不同,所以需要早封装CommonDAO的时候我们使用反省,这样就能根据new时所使用的参数来决定使用什么类型的参数


package com.qfedu.jdbc.utils;import java.sql.ResultSet;
import java.sql.SQLException;/*** @Descript 用于定义结果集映射的接口* @Author 千锋涛哥* 公众号: Java架构栈*/
public interface RowMapper<T> {public T getRow(ResultSet resultSet) throws SQLException;}
package com.qfedu.jdbc.utils;import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;/*** @Descript 公共DAO,用于封装公共的JDBC操作* @Author 千锋涛哥* 公众号: Java架构栈*/
public class CommonDAO<T> {/*** 公共DML操作*/public boolean update(String sql, Object... args){boolean b = false;try{Connection connection = DruidUtils.getConnection();PreparedStatement preparedStatement = connection.prepareStatement(sql);for (int i = 0; i < args.length ; i++) {preparedStatement.setObject(i+1,args[i]);}int i = preparedStatement.executeUpdate();b = i>0;}catch (Exception e){e.printStackTrace();}return b;}/*** 查询*/public List<T> select(String sql, RowMapper<T> rowMapper,Object...args){List<T> list = new ArrayList<>();try{Connection connection = DruidUtils.getConnection();PreparedStatement preparedStatement = connection.prepareStatement(sql);for (int i = 0; i <args.length ; i++) {preparedStatement.setObject(i+1,args[i]);}ResultSet resultSet = preparedStatement.executeQuery();while(resultSet.next()){// 从查询结果中取出一条记录(多个值),封装到一个实体类对象中//  getRow就是方法调用者在调用方法时,传递进来的用于映射查询结果的方法T t =  rowMapper.getRow(resultSet);list.add(t);}}catch (Exception e){e.printStackTrace();}return list;}}

基于通用查询的基本案例

DAO中的操作都是调用CommonDAO实现的

package com.qfedu.jdbc.dao;import com.qfedu.jdbc.dto.Book;
import com.qfedu.jdbc.utils.CommonDAO;
import com.qfedu.jdbc.utils.RowMapper;import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;/*** @Descript 对图书信息表进行CRUD操作* @Author 千锋涛哥* 公众号: Java架构栈*/
public class BookDAO {private CommonDAO<Book> commonDAO = new CommonDAO<>();public  boolean insertBook(Book book){String sql = "insert into books(book_name,book_author,book_price,book_stock,book_desc) values(?,?,?,?,?)";boolean b = commonDAO.update(sql, book.getBookName(), book.getBookAuthor(), book.getBookPrice(), book.getBookStock(), book.getBookDesc());return b;}public boolean deleteBook(int bookId){String sql = "delete from books where book_id=?";boolean b = commonDAO.update(sql, bookId);return b;}public boolean updateBook(Book book){String sql="update books set book_name=?,book_author=?,book_price=?,book_stock=?,book_desc=? where book_id=?";boolean b = commonDAO.update(sql, book.getBookName(), book.getBookAuthor(), book.getBookPrice(), book.getBookStock(), book.getBookDesc(), book.getBookId());return b;}public Book queryBook(int bookId){String sql = "select book_id,book_name,book_author,book_price,book_stock,book_desc from books where book_id=?";RowMapper<Book> bookRowMapper = new RowMapper<Book>(){public Book getRow(ResultSet resultSet) throws SQLException {int bid = resultSet.getInt("book_id");String bookName = resultSet.getString("book_name");String bookAuthor = resultSet.getString("book_author");double bookPrice = resultSet.getDouble("book_price");int bookStock = resultSet.getInt("book_stock");String bookDesc = resultSet.getString("book_desc");return new Book(bid,bookName,bookAuthor,bookPrice,bookStock,bookDesc);}};List<Book> list = commonDAO.select(sql, bookRowMapper, bookId);return list.size()>0?list.get(0):null;}public List<Book> listBooks(){String sql = "select book_id,book_name,book_author,book_price,book_stock,book_desc from books";List<Book> list = commonDAO.select(sql, resultSet -> {int bid = resultSet.getInt("book_id");String bookName = resultSet.getString("book_name");String bookAuthor = resultSet.getString("book_author");double bookPrice = resultSet.getDouble("book_price");int bookStock = resultSet.getInt("book_stock");String bookDesc = resultSet.getString("book_desc");return new Book(bid, bookName, bookAuthor, bookPrice, bookStock, bookDesc);});return list;}}

APache DBUtils

上面的commanDAO是我们自己封装的,但是其实市面上已经有了相应的封装,就是APache DBUtils我们可以直接引用

DButil的使用准备

11.3.1 创建Java应用
  • 创建Java工程
  • 添加驱动jar文件
11.3.2 创建连接池属性配置
  • 在src中创建package:com.qfedu.jdbc.utils

  • com.qfedu.jdbc.utils中创建druid.properties文件

  • 配置druid连接池的实行

  # 数据库连接信息driverClassName=com.mysql.cj.jdbc.Driverurl=jdbc:mysql://localhost:3306/db_test3?characterEncoding=utf8username=rootpassword=@QFedu123# 连接池属性# 连接池的初始化连接数<创建数据库连接池时默认初始化的连接的个数>initialSize=10# 连接池的最大连接数maxActive=50# 最小空闲连接数(当数据库连接使用率很低时,连接池中的连接会被释放一部分)minIdle=5# 超时等待时间(单位:ms)maxWait=30000
11.3.3 创建连接池工具类
  • 下载并导入druid的jar文件druid-1.2.8.jar

  • com.qfedu.jdbc.utils创建DruidUtils工具类(工具类与属性文件druid.properties在同目录下)

  package com.qfedu.jdbc.utils;import com.alibaba.druid.pool.DruidDataSource;import com.alibaba.druid.pool.DruidDataSourceFactory;import java.io.IOException;import java.io.InputStream;import java.sql.Connection;import java.sql.SQLException;import java.util.Properties;/*** @Descript Druid数据库连接池工具类* @Author 千锋涛哥* 公众号: Java架构栈*/public class DruidUtils {//1.定义DruidDataSource对象:表示Druid数据库连接池(数据源)private static DruidDataSource druidDataSource;//2.静态代码块初始化定义DruidDataSource对象static{try {//读取druid.properties文件中配置的属性InputStream is = DruidUtils.class.getResourceAsStream("druid.properties");Properties properties = new Properties();properties.load(is);//使用属性文件初始化DruidDataSource对象druidDataSource = (DruidDataSource) DruidDataSourceFactory.createDataSource(properties);} catch (IOException e) {e.printStackTrace();} catch (Exception e) {e.printStackTrace();}}//3.创建静态方法,从连接池对象中获取连接public static Connection getConnection(){Connection connection = null;try {connection =  druidDataSource.getConnection();} catch (SQLException e) {e.printStackTrace();}return connection;}}

DButil的使用

完成图书信息的数据库操作

  • 创建实体类
/*** @Descript 图书信息实体类* @Author 千锋涛哥* 公众号: Java架构栈*/
public class Book {private int bookId;private String bookName;private String bookAuthor;private double bookPrice;private int bookStock;private String bookDesc;}
13.3.1 添加操作
public int insertBook(Book book){int i= 0;try {//1.编写SQLString sql = "insert into books(book_name,book_author,book_price,book_stock,book_desc) values(?,?,?,?,?)";//2.准备参数Object[] params = {book.getBookName(),book.getBookAuthor(),book.getBookPrice(),book.getBookStock(),book.getBookDesc()};//3.调用commons-dbutils中的QueryRunner执行SQLQueryRunner queryRunner = new QueryRunner(DruidUtils.getDataSource());i = queryRunner.update(sql, params);} catch (SQLException e) {e.printStackTrace();}return i;
}
13.3.2 删除操作
public int deleteBook(int bookId){int i = 0;try {String sql = "delete from books where book_id=?";QueryRunner queryRunner = new QueryRunner(DruidUtils.getDataSource());i = queryRunner.update(sql,bookId);} catch (SQLException e) {e.printStackTrace();}return i;
}
13.3.3 修改操作
public int updateBook(Book book){int i=0;try {String sql = "update books set book_name=?,book_author=?,book_price=?,book_stock=?,book_desc=? where book_id=?";Object[] params = {book.getBookName(),book.getBookAuthor(),book.getBookPrice(),book.getBookStock(),book.getBookDesc(),book.getBookId()};QueryRunner queryRunner = new QueryRunner(DruidUtils.getDataSource());i = queryRunner.update(sql, params);} catch (SQLException e) {e.printStackTrace();}return i;
}
13.3.4 查询操作

查询一条记录

public Book queryBook(int bookId){Book book = null;try {String sql = "select book_id bookId,book_name bookName,book_author bookAuthor,book_price bookPrice,book_stock bookStock,book_desc bookDesc from books where book_id=?";QueryRunner queryRunner = new QueryRunner(DruidUtils.getDataSource());//1.对于查询操作,我们需要通过QueryRunner对象调用query方法来执行//2.所有的query方法都需要一个ResultSetHandler的参数,通过此参数指定query方法的返回类型//  如果SQL指令执行之后返回的是一行记录,我们通过BeanHandler指定查询结果封装的实体类类型//  要求:查询结果集的字段名必须与指定的实体类的属性名匹配//     方案1:创建实体类的时候,实体类中属性的名字与数据表中的列名相同//     方案2:查询语句字段取别名,让字段别名与实体类属性名一致book = queryRunner.query(sql, new BeanHandler<Book>(Book.class), bookId);} catch (SQLException e) {e.printStackTrace();}return book;
}

自定义结果集处理 自定义ResultSetHandler

public Book queryBook2(int bookId){Book book = null;try {String sql = "select book_id,book_name,book_author,book_price,book_stock,book_desc from books where book_id=?";QueryRunner queryRunner = new QueryRunner(DruidUtils.getDataSource());//1.对于查询操作,我们需要通过QueryRunner对象调用query方法来执行//2.所有的query方法都需要一个ResultSetHandler的参数,通过此参数指定query方法的返回类型//  如果SQL指令执行之后返回的是一行记录,我们通过BeanHandler指定查询结果封装的实体类类型//  要求:查询结果集的字段名必须与指定的实体类的属性名匹配//     方案3:自定义ResultSetHandler结果处理ResultSetHandler<Book> resultSetHandler = new ResultSetHandler<Book>() {@Overridepublic Book handle(ResultSet resultSet) throws SQLException {Book book = null;if(resultSet.next()) {int id = resultSet.getInt("book_id");String bookName = resultSet.getString("book_name");String bookAuthor = resultSet.getString("book_author");double bookPrice = resultSet.getDouble("book_price");int bookStock = resultSet.getInt("book_stock");String bookDesc = resultSet.getString("book_desc");book = new Book(id,bookName,bookAuthor,bookPrice,bookStock,bookDesc);}return book;}};book = queryRunner.query(sql, resultSetHandler, bookId);} catch (SQLException e) {e.printStackTrace();}return book;
}

查询多条记录

public List<Book> listBooks(){List<Book> bookList = null;try {String sql = "select  book_id bookId,book_name bookName,book_author bookAuthor,book_price bookPrice,book_stock bookStock,book_desc bookDesc  from books";QueryRunner queryRunner = new QueryRunner(DruidUtils.getDataSource());//如果SQL指令执行之后返回的是多行记录,我们通过BeanListHandler指定查询结果封装的实体类的集合类型bookList = queryRunner.query(sql, new BeanListHandler<Book>(Book.class));} catch (SQLException e) {e.printStackTrace();}return bookList;
}

查询一个值

例如在做分页的时候,我们需要查询数据的总记录数

public long getCount(){long count = 0;String sql = "select count(1) from books";QueryRunner queryRunner = new QueryRunner(DruidUtils.getDataSource());// 如果SQL指令执行之后返回的是一个值时,我们通过ScalarHandler指定返回类型// QueryRunner在处理统计操作时,是以long类型进行操作的,因此不能直接转成Integer// 如果我们确定这个值在int范围内,我们可以在得到long类型之后进行强转,建议使用long处理ScalarHandler<Long> scalarHandler = new ScalarHandler<Long>();try {count = queryRunner.query(sql, scalarHandler);} catch (SQLException e) {e.printStackTrace();}return count;
}

JavaWeb学习笔记(上)相关推荐

  1. JavaWeb学习笔记(十)--HttpServletRequest

    1. HttpServletRequest简介 HttpServletRequest对象代表客户端的请求,当客户端通过HTTP协议访问服务器时,HTTP请求头中的所有信息都封装在这个对象中 2. Ht ...

  2. B站狂神说JavaWeb学习笔记

    JavaWeb学习笔记(根据b站狂神说java编写) 1.基本概念 1.1 前言 静态Web: 提供给所有人看数据不会发生变化! HTML,CSS 动态Web: 有数据交互,登录账号密码,网站访问人数 ...

  3. JavaWeb学习笔记(5)-B站尚硅谷

    文章目录 十四.书城项目第三阶段--优化 (1)页面jsp动态化 (2)抽取页面中相同的内容 A.登录成功的菜单 B.base.css.jQuery标签 C.每个页面的页脚 D.manager模块的菜 ...

  4. javaweb学习笔记2(jquery的使用,以及常用的方法,选择器,过滤器)

    javaweb学习笔记2 javascript正则表达式 regfxp对象 方式1: var putt=new RegExp("e");//表示要求字符串中必须包含字符串evar ...

  5. 【javaweb学习笔记】servlet-api,filter和Listener

    javaweb学习笔记 1. servlet-api 1.1 servlet初始化 1.2 ServletContext和context-param 2. 什么是业务层 3. IOC 3.1 耦合/依 ...

  6. java web孤傲苍狼,JavaWeb学习笔记

    我看的资料是孤傲苍狼的javaweb学习笔记,他写的真的很全,这或许就是社区力量吧!哪些问题不知道,上网搜一搜就有了,让自己不进步的敌人,只有懒惰了. 这是我接触JavaWeb的第二周,有一些自己的理 ...

  7. Javaweb学习笔记(JSP标准标签库)

    Javaweb学习笔记(JSP标准标签库) JSTL入门 安装和测试JSTL JSTL中的Core标签库 < c:out>标签 标签 标签 < c:catch>标签 标签 标签 ...

  8. JavaWeb学习笔记(数据库、SQL语句、数据查询语法、完整性约束、编码、备份和恢复数据、多表查询)

    数据库.SQL语句.数据查询语法.完整性约束.编码.备份和恢复数据.多表查询 JavaWeb学习笔记 数据库 数据库概念 基本命令 启动和关闭mysql服务器 客户端登录退出mysql SQL语句 S ...

  9. javaWeb学习笔记1—前端三件套 HTML CSS JavaScript

    学习视频地址 javaWeb学习笔记-前端三件套 HTML CSS JavaScript 1.字体标签 2. 字符实体 3.标题标签 4.超链接 5.列表标签 6. img标签 路径 7.表格 8.i ...

  10. JavaWeb学习笔记(软件系统体系结构、Tomcat、Web应用、HTTP协议)

    JavaWeb学习笔记 JavaWeb学习笔记 软件系统体系结构 常见软件系统体系结构C/S.B/S Web资源 Web服务器 Tomcat Tomcat概述 安装.启动.配置Tomcat Web应用 ...

最新文章

  1. linux异常断电usb驱动丢失,如何修复Linux中损坏的USB驱动器 | MOS86
  2. Python使用中文注释和输出中文(原创)
  3. 请你说明ConcurrentHashMap有什么优势以及1.7和1.8区别?
  4. Python 中实用却不常见的小技巧!
  5. 【机器视觉】 gen_measure_arc算子
  6. 从零开始学视觉Transformer(5):如何训练ViT模型、DeiT算法解析
  7. 经典算法——斐波那契数列
  8. js如何获取php中的变量的类型,js获取变量的类型
  9. 《机械原理》上 学后感
  10. java小球碰撞界面设计_JavaScript实现小球碰撞特效
  11. 使用命令启动IOS模拟器
  12. 图神经网络入门:GCN论文+源码超级详细注释讲解!
  13. 大数据/人工智能实验室建设优势
  14. linux snoop抓包命令,Snoop抓包工具用法简介.doc
  15. c语言程序设计21点扑克牌,C语言程序设计21点扑克牌游戏.doc
  16. Eli Lilly(礼来) | RPA在医疗行业的应用案例
  17. dbt-tidb 1.2.0 尝鲜
  18. 大师速写作品及理论,有你喜欢的知识
  19. 如何防止QT程序未响应
  20. Ctrl + Alt + Left/Right键失效以及Ctrl + Space键被占用解决

热门文章

  1. 用AlexNet训练MSTAR数据集
  2. Java游戏项目之黄金矿工
  3. 纯css的大于号样式
  4. 公司倒闭 1 年了,而我当年的项目上了 GitHub 热榜
  5. 520送什么礼物好呢、适合送女友的礼物推荐
  6. 英特尔推出SD卡巨细电脑 配Atom处理器
  7. 搭建GitHub免费个人网站(详细教程)
  8. 报表工具(报表设计器)使用的开发历程
  9. centos7.6安装maven
  10. 日本知名汽车零部件公司巡礼系列之株式会社67