Java EE--框架篇(3-1)Mybatis

i18n486發表於2020-11-26

目錄

前言

Hibernate/Mybatis

快速入門Mybatis

單表增刪改查

單表條件查詢

***:#{param}和${param}的區別是?

***:查詢結果是自定義型別,怎麼辦?

多表聯合查詢

分頁查詢


前言

帶著問題學java系列博文之java基礎篇。從問題出發,學習java知識。


Hibernate/Mybatis

前面無框架開發後臺服務時(《Java EE--無框架開發後臺服務》),資料庫操作這塊,沒有藉助任何框架,直接就是JDBC程式設計,整個需求實現下來可以說是痛苦萬分。

1.每次運算元據庫都要建立資料庫連線,執行操作後又要釋放連線,存在頻繁的資料庫連線建立、銷燬;造成資源消耗,效率低下;

2.所有的操作都需要編寫sql語句,並完成引數和sql語句的拼接,沒有與實體類物件關聯起來,操作複雜;

3.查詢之後,需要遍歷ResultSet結果集,完成結果集與物件實體的封裝;

4.對於查詢結果集、重複查詢等沒有任何快取,每一次都需要從資料庫查詢資料;

5.不支援自動建表、更新表。

上面總結的痛點相信經歷過純手工JDBC程式設計的都深有體會,為了解決這些問題,所以推出了ORM(Object relationship mapping 物件關係對映)框架,旨在採用池技術統一管理資料庫連線session、connection;自動封裝物件實體類與資料表之間的關聯(實體類與資料表關聯,查詢結果自動封裝為具體物件);對查詢結果可配置快取(一級、二級快取),對於重複查詢可以從快取中獲取,提高效率;全自動的ORM框架還支援自動建表、更新表等。簡單的說,有了ORM框架,將會發現資料庫程式設計這塊會徹底解放,甚至都不需要程式設計師去編寫sql,一切都由框架準備好了。

下面就帶領大家快速上手這兩大框架,也經典的後臺架構SSM/SSH中的第三個框架。(本博文都是基於SpringBoot(SpringBoot框架可以看成是Spring SpringMVC集合,後續博文會單獨講解),以全註解舉例。如果想要詳細學習框架,建議還是從配置階段開始,逐漸過渡到全註解階段。當然如果只是為了使用它,那直接學習全註解,能上手開發即可)

 

快速入門Mybatis

建表語句:

CREATE TABLE `school`.`student` (
  `id` INT NOT NULL AUTO_INCREMENT COMMENT '學生編號',
  `name` VARCHAR(45) NOT NULL COMMENT '姓名',
  `age` INT(11) NOT NULL COMMENT '年齡',
  `address` VARCHAR(100) NULL COMMENT '居住地址',
  PRIMARY KEY (`id`))
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8
COMMENT = '學生表';

CREATE TABLE `school`.`grades` (
  `id` INT NOT NULL AUTO_INCREMENT COMMENT '主鍵',
  `student_id` INT NOT NULL COMMENT '外來鍵關聯學生表',
  `math` INT NOT NULL COMMENT '數學成績',
  `chinese` INT NOT NULL COMMENT '語文成績',
  `english` INT NOT NULL COMMENT '英語成績',
  PRIMARY KEY (`id`),
  INDEX `student_id_idx` (`student_id` ASC) VISIBLE,
  CONSTRAINT `studentId`
    FOREIGN KEY (`student_id`)
    REFERENCES `school`.`student` (`id`)
    ON DELETE CASCADE
    ON UPDATE CASCADE)
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8
COMMENT = '成績表';

整合對接:

專案結構圖:

1.實體類

@Data
@Table(name = "grades")
public class Grades implements Serializable {

    @Id
    @KeySql(useGeneratedKeys = true)
    private Integer id;

    @Column(name = "student_id")
    private Integer studentId;
    private Integer math;
    private Integer chinese;
    private Integer english;
}

@Data
@Table(name = "student")
public class Student {
    @Id
    @KeySql(useGeneratedKeys = true)  //mybatis通用mapper的註解,表明自增屬性
    private Integer id;

    private String name;
    private Integer age;
    private String address;
}

2.mapper介面(依賴通用mapper實現)

public interface StudentMapper extends Mapper<Student> {
}

public interface GradesMapper extends Mapper<Grades> {
}

3.啟動類新增註解掃描mapper

@SpringBootApplication
@MapperScan("com.zst.learnmybatis.mapper") //配置要掃描的mapper包名
public class LearnmybatisApplication {
    public static void main(String[] args) {
        SpringApplication.run(LearnmybatisApplication.class, args);
    }
}

4.配置資訊

##日誌中列印具體的sql語句
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
##指定掃描的實體類包名
mybatis.type-aliases-package=com.zst.learnmybatis.domain

##使用druid資料庫連線池
spring.datasource.druid.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.druid.url=jdbc:mysql://127.0.0.1:3306/school?useSSL=true&characterEncoding=utf-8&serverTimezone=GMT
spring.datasource.druid.username=root
spring.datasource.druid.password=***

5.所需依賴

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.0.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>tk.mybatis</groupId>
            <artifactId>mapper-spring-boot-starter</artifactId>
            <version>2.0.3</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.3</version>
        </dependency>
    </dependencies>

至此,springboot整合Mybatis(依賴通用mapper啟動器)完畢,這裡用了druid連線池,tk.mybatis.mapper。用通用mapper的目的,是為了簡化編碼,因為通用mapper基本實現了單表操作的所有方法,可以直接呼叫。

 

單表增刪改查

@SpringBootTest
@RunWith(SpringRunner.class)
class StudentMapperTest {

    @Autowired
    StudentMapper studentMapper;

    //單表增刪改查
    @Test
    public void testInsert(){
        Student student = new Student();
        student.setName("王五");
        student.setAge(18);
        student.setAddress("合肥");
        studentMapper.insert(student);
    }
}

單表增刪改查等基本操作,可以直接使用通用mapper的基礎方法,是不是非常簡便。我們什麼都沒做,但是框架已經為我們實現所有!

下面梳理一下mapper中基本的增刪改查方法:

新增方法:

mapper給我們預設實現了兩個方法:insert()和insertSelective();它們分別的意思是:

insert():表示向表中插入一條資料,表列值就是該物件的屬性值。如果有自增主鍵,且物件例項的主鍵屬性也有值,則直接插入,如果物件主鍵屬性沒有值,則由資料庫根據表資料自動生成。

insertSelective():區別insert(),這個方法僅給屬性值不為null的列設定值,如果物件例項屬性為null或者未設定值,則不做任何操作,表資料該列值由資料庫預設填充。

 

刪除方法:

mapper給我們實現了三個刪除方法:deleteByPrimaryKey()、delete()、deleteByExample()

deleteByPrimaryKey():這個方法就如其名,根據主鍵刪除;

delete():這個方法相當於條件刪除,即刪除表記錄中列值符合物件例項屬性值的所有記錄(物件例項的屬性值相當於and拼接的條件);

deleteByExample():這個方法完全就是條件刪除,其中傳入的example就是mybatis定義的專門用於條件拼接的類。

 

修改方法:

mapper給我們實現了四個修改方法:updateByPrimaryKey()、updateByExample()、updateByExampleSelective()、updateByPrimaryKeySelective()

updateByPrimaryKey():根據主鍵更新表記錄,實體屬性空值也會設定到資料表對應列;

updateByPrimaryKeySelective():根據主鍵更新表記錄,實體屬性空值也不會設定到資料表對應列,對應列的值由資料庫已有值或者預設填充;

updateByExample():根據條件更新,相當於更新所有符合條件的表記錄,實體屬性值對應表的列,空值也會設定;

updateByExampleSelective():根據條件更新,相當於更新所有符合條件的表記錄,實體屬性值對應表的列,空值不會設定,由資料庫已有值或者預設填充;

 

查詢方法:

查詢方法內建有很多個,分別解釋一下意思:

select():將傳入的實體類屬性值作為條件,進行查詢;

selectAll():查詢表中所有記錄;

selectByExample():根據傳入的Example進行條件查詢;

selectByRowBounds():將傳入的實體類屬性值作為條件,拼接上RowBounds的取結果限制(分頁),進行查詢;這個方法就是為了分頁準備的,RowBounds支援兩個引數offset和limit;

selectByExampleAndRowBounds():將傳入的Example作為條件,拼接上RowBounds的取結果限制(分頁),進行查詢;(分頁方法)

selectByPrimaryKey():根據主鍵查詢;

selectCount():查詢記錄總數;

selectCountByExample():根據example條件查詢,查詢記錄總數;

selectOne():將傳入的實體類屬性值作為條件,進行查詢;必須確保符合條件的記錄僅有一條,否則將會報錯;

selectOneByExample():將傳入的Example作為條件,進行查詢,必須確保符合條件的記錄僅有一條,否則將會報錯;

 

單表條件查詢

@SpringBootTest
@RunWith(SpringRunner.class)
public class StudentMapperTest {
    //單表條件查詢
    @Test
    public void testExample(){
        Example example = new Example(Student.class);
        example.createCriteria().andLike("name","%二%")
                .orLike("address","%肥%");
        List<Student> students = studentMapper.selectByExample(example);
        for (Student student : students) {
            System.out.println(student);
        }
    }
}

如上,使用Example物件,拼接條件,然後進行條件查詢。上例演示了查詢姓名中含有“二”或者地址中含有“肥”的學生。

呼叫Example物件的createCriteria()方法,建立一個條件物件後,就可以呼叫相關方法,進行條件拼接。Example.Criteria物件具體方法如下:

addCriterion()、addOrCriterion() :新增自定義的條件(or 等同於 sql語句的or:或運算連線)

andIsNull()、andIsNotNull():值是空/非空(and 等同於 sql語句的and:與運算連線;類似方法,or連線符)

andEuqalTo()、andNotEqualTo():相等/不相等(類似方法,or連線符)

andLessThan()、andLessThanOrEqualTo():小於/小於等於(類似方法,or連線符)

andIn()、andNotIn():在取值範圍內/不在範圍內(類似方法,or連線符)

andBetween()、andNotBetween():在取值範圍內/不在範圍內(類似方法,or連線符)

andLike()、andNotLike():含有/不含有條件值(類似方法,or連線符)

 

也可以直接用Example的方法進行條件拼接:

@SpringBootTest
@RunWith(SpringRunner.class)
public class StudentMapperTest {
    //單表條件查詢
    @Test
    public void testExample(){
        Example example = Example.builder(Student.class).andWhere(Sqls.custom().andLike("name","%二%")
                .orLike("address","%肥%")).orderByDesc("id").build();
        studentMapper.selectByExample(example);
    }
}

如上還加上了排序,演示了查詢姓名中含有“二”或者地址中含有“肥”的學生,並按照id降序輸出。

 

單表條件查詢Example中的方法涵蓋了所有情形嗎?如果有些沒有涵蓋怎麼辦呢?

其實單表查詢,使用Example基本可以滿足所有場景了。如果真的遇到Example或者Example.Criteria中沒有的,也可以通過手寫sql來實現,範例如下:

public interface StudentMapper extends Mapper<Student> {

    @Select("select * from student where name like #{name} or address like #{address} order by id desc")
    List<Student> likeNameOrAddressOrderByIdDesc(String name,String address);
}

即只需要在Mapper介面中增加方法,編寫sql語句就好。上例的方法和使用Example查詢效果完全一致。

***:#{param}和${param}的區別是?

上例編寫sql語句時,引數設定這塊用了一個符號表示式#{name},它的意思是這裡使用引數name。而mybatis還有另外一種符號表示式:${param}

如果上例使用這個表示式的話,具體如下:

select * from student where name like '%${name}%' or address like '%${address}%' order by id desc

可以看到${param}表示式,相當於字串佔位符,僅僅是將引數作為字串填充進sql語句。

從這兩個sql語句可以看出來,很明顯使用#{param}更好一點,原因很簡單:使用#{param}相當於使用了PrepareStatement,向其中新增引數,執行效率更高,而且沒有sql注入漏洞;而使用${param}相當於使用了statement,只是一個字串填充,執行效率低,且存在sql注入漏洞。因此推薦使用#{param}表示式,不過要注意,像本例中like查詢的時候,需要對傳參name手動加上“%%”,然後再作為方法傳參。

***:查詢結果是自定義型別,怎麼辦?

mapper中自帶的方法返回型別都是實體類或者實體類集合等,如果我們要求返回自定義型別,該怎麼辦呢?顯然此時就無法使用mapper自帶的方法了,需要我們在mapper介面類中編寫sql,自定義返回型別,如下例:

public interface StudentMapper extends Mapper<Student> {

    @Select("select name,age,address from student where id = #{id}")
    @ResultType(StudentDto.class) //可以使用註解表明返回型別,也可以省略不寫
    StudentDto queryByPrimaryKey(Integer id);
}


/**
 * 自定義返回型別實體類
 * 注意屬性名要和查詢語句返回的列名一一對應,否則將無法轉化
 * 本質:框架替我們封裝了返回結果而已
 * ResultSet.getString("name")->name屬性值
 * ResultSet.getString("address")->address屬性值
 * ResultSet.getInt("age")->age屬性值
 */
@Data
@ToString
public class StudentDto {
    private String name;
    private Integer age;
    private String address;
}


@SpringBootTest
@RunWith(SpringRunner.class)
public class StudentMapperTest {
    //自定義型別
    @Test
    public void testDefine(){
        StudentDto studentDto = studentMapper.queryByPrimaryKey(1);
        System.out.println(studentDto.toString());
    }
}

 

多表聯合查詢

通用mapper只有當前類對應的單表操作系列方法,如果要實現多表聯合查詢,則必須在mapper介面類中增加相應方法,編寫sql語句。如下例:

@Data
@ToString
public class StudentScoreDto {
    private String name;
    private Integer math;
    private Integer chinese;
    private Integer english;
}

public interface StudentMapper extends Mapper<Student> {
    @Select("select s.name,g.math,g.chinese,g.english from student s left join grades g on s.id = g.student_id where s.id = #{id}")
    StudentScoreDto queryStudentScoreById(Integer studentId);
}

@SpringBootTest
@RunWith(SpringRunner.class)
public class StudentMapperTest {
    //多表查詢
    @Test
    public void testMulti(){
        StudentScoreDto studentScoreDto = studentMapper.queryStudentScoreById(2);
        System.out.println(studentScoreDto.toString());
    }
}

本例演示了查詢id為2的學生的姓名以及各科的分數,用到了自定義型別,以及編寫sql語句(左連查詢)。

 

分頁查詢

上面單表查詢時分析過,mapper給我們寫好了方法,支援分頁查詢,如下例:

@SpringBootTest
@RunWith(SpringRunner.class)
public class StudentMapperTest {    
    //分頁查詢
    @Test
    public void testPageQuery(){
        List<Student> students = studentMapper.selectByRowBounds(null, new RowBounds(3, 5));
        for (Student student : students) {
            System.out.println(student);
        }
    }
}

從上圖可以看到其實還是查詢了所有資料,只是在返回資料時,框架給我們做了資料篩除。其它的藉助RowBounds物件實現分頁查詢方法也類似。另外使用這個方法有個缺陷,那就是需要我們每次計算出offset(偏移量),然後作為引數出入方法。顯然,這種查詢效率低,還需要我們每次都計算偏移量的分頁方式很不友好。為此,業內出了各種幫助分頁查詢的小外掛框架,比較著名的有PageHelper,下面以PageHelper舉例:

@SpringBootTest
@RunWith(SpringRunner.class)
public class StudentMapperTest {
    //分頁查詢
    @Test
    public void testPageQueryByHelper(){
        //開啟分頁
        PageHelper.startPage(2,5);
        List<Student> students = studentMapper.selectAll();
        PageInfo<Student> pageInfo = new PageInfo<>(students);
        for (Student student : students) {
            System.out.println(student);
        }
        System.out.format("total:%s,pageSize:%s,pageNum:%s",pageInfo.getTotal(),pageInfo.getPageSize(),pageInfo.getPageNum());
        System.out.println();
        for (Student student : pageInfo.getList()) {
            System.out.println(student);
        }
    }
}

可以看到,使用PageHelper進行分頁查詢,雖然看起來還是呼叫mapper的selectAll()查詢所有,但其實是由PageHelper接管了sql語句,分別執行了兩次sql查詢,首先查詢出記錄總數,然後PageHelper會根據傳入的引數(PageNum 頁碼,PageSize 每頁記錄數)計算出偏移量,作為sql查詢 limit 語句後的引數填入。進行第二次查詢,獲得分頁資料。PageHelper雖然分為兩次查詢,但是兩次都是小資料量返回,對於表資料非常多的情形,很明顯比一次查詢所有記錄效率要高得多,且佔用記憶體也更小。因此推薦使用PageHelper進行分頁查詢,不建議使用mapper自帶的方法。


以上繫個人理解,如果存在錯誤,歡迎大家指正。原創不易,轉載請註明出處!

相關文章