Spring Boot integracija MyBatis-a
Suština ORM frameworka jeste da pojednostavi kodiranje rada sa bazom podataka; često se koriste dva frameworka - MyBatis, koji fleksibilno izvršava dinamički SQL, i Hibernate, koji se zalaže za pisanje bez SQL-a. Prvi se više koristi u internet industriji, a drugi u tradicionalnoj industriji.
Karakteristika Hibernate-a jeste da sav SQL nastaje iz Java koda; na vrhu te linije razvoja nalazi se Spring Data JPA, u kojoj u suštini samo na osnovu imena metoda može da se generiše odgovarajući SQL.
MyBatis je nekadašnje vreme bio prilično napornog za korišćenje - zahtevao je razne konfiguracione fajlove i mapiranje između klasa entiteta i DAO-a; nakon neprekidnog razvoja i poboljšanja, danas se pomoću generator-a automatski mogu generisati klase entiteta, konfiguracioni fajlovi i DAO sloj, što je znatno olakšalo razvoj.
Pojavom MyBatis-Plus-a, razvoj MyBatis-a dodatno je ubrzan. Uz poboljšanje MyBatis-Plus-a, programer uz jednostavnu konfiguraciju brzo može raditi CRUD operacije nad jednom tabelom; istovremeno, MyBatis-Plus nudi bogate funkcije kao što su generisanje koda, automatska paginacija, logičko brisanje, automatsko popunjavanje i sl., što dodatno pojednostavljuje razvoj.
Integracija MyBatis-a
Prvi korak: u fajl pom.xml uvedi starter.
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>Drugi korak: u fajl application.yml dodaj konfiguraciju za konekciju sa bazom.
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: Huicheng123**
url: jdbc:mysql://localhost:3306/codingmore-mybatis?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&useSSL=falseTreći korak: uvezi SQL fajl.

Četvrti korak: napravi klasu entiteta User.java.
@Data
@Builder
public class User {
private Integer id;
private Integer age;
private String name;
private String password;
@Tolerate
User() {}
}Ovde su korišćene lombok anotacije
- @Data automatski generiše getter/setter
- @Builder generiše lance (fluent) pozive
- Pošto kombinacija @Data i @Builder gubi bezargumenski konstruktor, aktivno smo deklarisali bezargumenski konstruktor i pomoću anotacije
@Toleraterekli lombok-u da dozvoli njegovo postojanje (odsustvo bezargumenskog konstruktora dovodi do greške pri ORM mapiranju)
Peti korak: napravi interfejs UserMapper.java:
public interface UserMapper {
@Select("SELECT * FROM user")
List<User> getAll();
@Select("SELECT * FROM user WHERE id = #{id}")
User getOne(Integer id);
@Insert("INSERT INTO user(name,password,age) VALUES(#{name}, #{password}, #{age})")
void insert(User user);
@Update("UPDATE user SET name=#{name},password=#{password},age=#{age} WHERE id =#{id}")
void update(User user);
@Delete("DELETE FROM user WHERE id =#{id}")
void delete(Integer id);
}- @Select anotacija služi za upit
- @Insert anotacija služi za umetanje
- @Update anotacija služi za izmenu
- @Delete anotacija služi za brisanje
Šesti korak: na klasu za pokretanje CodingmoreMybatisApplication dodaj anotaciju @MapperScan koja skenira mapper.
@SpringBootApplication
@MapperScan
public class CodingmoreMybatisApplication {
public static void main(String[] args) {
SpringApplication.run(CodingmoreMybatisApplication.class, args);
}
}Ako nije navedena putanja skeniranja za @MapperScan, skeniranje kreće od paketa klase u kojoj je anotacija navedena.
Sedmi korak: u testnoj klasi testiraj mapper.
@SpringBootTest
@Slf4j
class CodingmoreMybatisApplicationTests {
@Autowired
private UserMapper userMapper;
@Test
void testInsert() {
userMapper.insert(User.builder().age(18).name("Chenmo Wang Er").password("123456").build());
userMapper.insert(User.builder().age(18).name("Chenmo Wang San").password("123456").build());
userMapper.insert(User.builder().age(18).name("Chenmo Wang Si").password("123456").build());
log.info("Dohvati sve: {}",userMapper.getAll().stream().toArray());
}
@Test
List<User> testQuery() {
List<User> all = userMapper.getAll();
log.info("Dohvati sve: {}",all.stream().toArray());
return all;
}
@Test
void testUpdate() {
User one = userMapper.getOne(1);
log.info("Pre izmene{}", one);
one.setPassword("654321");
userMapper.update(one);
log.info("Posle izmene{}", userMapper.getOne(1));
}
@Test
void testDelete() {
log.info("Pre brisanja{}", userMapper.getAll().toArray());
userMapper.delete(1);
log.info("Posle brisanja{}", userMapper.getAll().toArray());
}
}Minimalna xml verzija
Minimalna xml verzija pogodnija je za složenije SQL - interfejsni sloj definiše samo prazne metode, a zatim se u xml-u piše odgovarajući SQL. U praktičnom projektu Codingmore 🐱 koristi se upravo taj pristup.
Prvi korak: napravi PostMapper.
public interface PostMapper {
List<Posts> getAll();
Posts getOne(Long id);
void insert(Posts post);
void update(Posts post);
void delete(Long id);
}Drugi korak: u direktorijum resources napravi fajl PostMapper.xml.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="top.codingmore.mapper.PostMapper">
<resultMap id="BaseResultMap" type="top.codingmore.entity.Posts">
<id column="posts_id" property="postsId"/>
<result column="post_author" property="postAuthor"/>
<result column="post_content" property="postContent"/>
<result column="post_title" property="postTitle"/>
</resultMap>
<sql id="Base_Column_List">
posts_id, post_author, post_content, post_title
</sql>
<select id="getAll" resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from posts;
</select>
<select id="getOne" parameterType="java.lang.Long" resultMap="BaseResultMap" >
SELECT
<include refid="Base_Column_List" />
FROM users
WHERE id = #{id}
</select>
<insert id="insert" parameterType="top.codingmore.entity.Posts">
insert into
posts
(post_author,post_content,post_title)
values
(#{postAuthor},#{postContent},#{postTitle});
</insert>
<update id="update" parameterType="top.codingmore.entity.Posts">
update
posts
set
<if test="postAuthor != null">post_author=#{postAuthor},</if>
<if test="postContent != null">post_content=#{postContent},</if>
post_title=#{postTitle}
where id=#{id}
</update>
<delete id="delete">
delete from
posts
where
id=#{id}
</delete>
</mapper>SQL koji odgovara metodama interfejsa direktno se piše u xml fajlu; konkretan položaj vidi na donjoj slici:

Možeš i fajl smestiti u direktorijum na istom nivou sa interfejsom PostMapper.java, ali to donosi jedan problem - Maven prilikom pakovanja podrazumevano ignoriše xml fajlove, pa da bismo izbegli tu situaciju, u fajl pom.xml moramo dodati konfiguraciju:
<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
<resource>
<directory>src/main/resources</directory>
</resource>
</resources>
</build>Ako fajl smestiš direktno u direktorijum resources, ne moraš da brineš o tome da će ga pakovanje preskočiti, ali ga iz resources direktorijuma MyBatis neće automatski skenirati, pa u konfiguracionom fajlu application.yml MyBatis-u treba navesti konkretan putanja skeniranja:
mybatis:
mapper-locations: classpath:mapper/*.xmlTreći korak: u testnoj klasi dodaj testne metode:
@Test
void testPostInsert() {
postMapper.insert(Posts.builder()
.postAuthor(1L)
.postTitle("Chenmo Wang Er")
.postContent("123456")
.build());
log.info("Dohvati sve: {}",postMapper.getAll().stream().toArray());
}
@Test
List<Posts> testPostQuery() {
List<Posts> all = postMapper.getAll();
log.info("Dohvati sve: {}",all.stream().toArray());
return all;
}
@Test
void testPostUpdate() {
Posts one = postMapper.getOne(1L);
log.info("Pre izmene{}", one);
one.setPostContent("Chenmo Wang Er je budala");
postMapper.update(one);
log.info("Posle izmene{}", postMapper.getOne(1L));
}
@Test
void testPostDelete() {
log.info("Pre brisanja{}", postMapper.getAll().toArray());
postMapper.delete(1L);
log.info("Posle brisanja{}", postMapper.getAll().toArray());
}Može se zaključiti da je anotirana verzija pogodnija za jednostavne SQL naredbe; čim naiđe na složenije SQL upite, poput upita nad više tabela, pisanje SQL-a u xml-u daleko je lakše za implementaciju.
Na primer, u praktičnom projektu Codingmore 🐱 postoji paginirani upit (prikaz početne stranice, gde treba dohvatiti oznake, ime autora, informacije o članku itd.) koji obuhvata više tabela, pa je tada xml verzija pogodnija.
<select id="findByPageWithTagPaged" resultMap="PostsVoResultMapWithTagList">
SELECT a.*, pt.description, ptr.post_tag_id
FROM (
SELECT
<include refid="Base_Column_List_No_Content" />,
b.term_taxonomy_id,
c.user_nicename
FROM
posts a
LEFT JOIN term_relationships b ON a.posts_id = b.term_relationships_id
LEFT JOIN users c ON a.post_author = c.users_id
WHERE 1=1
<if test="searchTagId != null">
and a.posts_id in (select post_id from post_tag_relation where post_tag_id=#{searchTagId})
</if>
and ${ew.sqlSegment}
LIMIT #{pageStart}, #{pageSize}
) a
LEFT JOIN post_tag_relation ptr on a.posts_id = ptr.post_id
LEFT JOIN post_tag pt on pt.post_tag_id = ptr.post_tag_id
</select>Pažljiviji čitač sigurno je primetio izraz poput ${ew.sqlSegment} - on pripada sadržaju MyBatis-Plus-a.
Poboljšanje pomoću MyBatis-Plus-a
MyBatis je poluautomatski ORM framework - i za jednostavan CRUD potrebno je pisati SQL naredbe, pa ako želiš da preskočiš te korake, možeš izabrati JPA, a možeš i MyBatis-Plus (skraćeno MP), koji su razvili naši ljudi.
MP pruža niz odličnih osobina, na primer:
- Moćne CRUD operacije: ugrađeni su opšti mapper i service; uz malo konfiguracije može se izvesti većina uobičajenih CRUD operacija, bez pisanja SQL naredbi.
- Podrška za automatsko generisanje primarnog ključa
- Podrška za ActiveRecord obrazac: klasa entiteta samo treba da nasledi klasu Model i može da radi snažne CRUD operacije
- Moćan generator koda: brzo generiše Mapper, Model, Service, Controller sloj
- Ugrađen plugin za paginaciju
- Ugrađen plugin za analizu performansi: može ispisivati SQL naredbe i njihovo vreme izvršenja
Hajde da uđemo u praksu.
Prvi korak: u fajl pom.xml dodaj starter za MyBatis-Plus.
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>Drugi korak: napravi klasu entiteta PostTag.
@Data
public class PostTag {
private Long postTagId;
private String description;
}Odgovarajuća tabela u bazi je post-tag.

Vidi se da ime klase PostTag, imena polja postTagId i tabela u bazi post_tag, polje post_tag_id nisu ista, ali nas MP automatski povezuje u mapiranju.
Treći korak: napravi PostTagMapper koji nasleđuje BaseMapper; nakon nasleđivanja tog interfejsa, bez pisanja mapper.xml fajla, dobijamo CRUD funkcionalnost.
public interface PostTagMapper extends BaseMapper<PostTag> {}Metodi koje BaseMapper pruža su sledeći:

Treći korak: u testnu klasu dodaj metod za upit.
Putanja do izvornog koda
- Codingmore: https://github.com/itwanger/coding-more
- codingmore-mybatis: https://github.com/itwanger/codingmore-learning
