Однажды, когда мне нужно было снова создать очень простую таблицу базы данных из-за определенной функции, а затем написать для нее CRUD-операции, я окончательно не выдержал. Что касается написания кода, мой последовательный принцип заключается в том, чтобы писать меньше кода и меньше повторяющегося кода, и эти XML-конфигурации с аналогичными добавлениями, удалениями, модификациями и проверками являются для меня бессмысленной и повторяющейся физической работой. Это неприемлемо для меня.
Думая, что при использовании Spring Data JPA нужно только объявить интерфейс, и сразу доступны методы добавления, удаления, изменения и проверки, а для некоторых простых запросов можно объявить метод интерфейса через имя метода в конкретном формат. Однако JPA основан на спящем режиме, который неэффективен и негибок, поэтому MyBatis является инфраструктурой ORM для большинства предприятий, поэтому я давно отказался от JPA.
Итак, могу ли я создать проект, подобный Spring Data JPA, поверх MyBatis, чтобы выполнять функции, подобные JPA? Он может не только иметь простоту JPA, но и поддерживать гибкость и эффективность Mybatis. Первоначальная идея была основана на исходном коде Spring Data JPA, но после прочтения исходного кода JPA я отказался от этой идеи, слишком много кода. Позже я случайно наткнулся на проект Mybatis Plus, прочитав его документацию, у меня вдруг возникла идея, и я решил начать работу над ним на базе Mybatis Plus.
Особенности проекта:
- Поддерживает автоматический вывод операций базы данных, таких как добавление, запрос, изменение, удаление, подсчет и существование, на основе имен методов DAO.
- Поддержка различных форм выражений, таких как findById, queryById, selectById эквивалентны, deleteById и removeById эквивалентны
- Поддерживает автоматический анализ resultMap в соответствии со структурой объекта (объекты, поддерживающие каскадирование), больше не нужно настраивать resultMap в xml-файле.
- Поддержка вывода соединения, сложный SQL также может быть автоматически выведен
- Поддерживает операции подкачки, поддерживает разбиение по страницам и сортировку объектов Pageable данных Spring.
- Поддержка объектов Pageable и Page данных Spring, которые можно легко переключать с помощью jpa.
- Поддерживает некоторые аннотации jpa: @Table, @Transient, @Id, @GeneratedValue для постоянных объектов.
- Поддержка автоинкрементного заполнения первичного ключа, необходимо добавить аннотацию jpa @GeneratedValue к атрибуту первичного ключа.
Идеи дизайна
Инжектор Sql с использованием MyBatis Plus
Все начинается здесь:
override fun getMethodList(): List<AbstractMethod> {
return listOf(
UnknownMethods()
)
}
Здесь вводится только один метод.Согласно дизайнерской идее Mybatis Plus, метод отвечает только за sql-инъекцию метода с определенным именем, но, читая код AbstractMethod, мы знаем, что на самом деле любое число объявлений sql можно ввести в метод, см. следующий код:
/**
* 添加 MappedStatement 到 Mybatis 容器
*/
protected MappedStatement addMappedStatement(Class<?> mapperClass, String id, SqlSource sqlSource,
SqlCommandType sqlCommandType, Class<?> parameterClass,
String resultMap, Class<?> resultType,
KeyGenerator keyGenerator, String keyProperty, String keyColumn) {
...
}
С помощью этого метода вы можете вводить произвольные операторы sql.
Оглядываясь назад на вышесказанное, я только внедрил метод внедрения UnknownMethods, который является точкой входа для всех функций этого проекта. Кода в этом классе не так уж и много, поставлю прямо
override fun injectMappedStatement(mapperClass: Class<*>, modelClass: Class<*>, tableInfo: TableInfo): MappedStatement {
// 修正表信息,主要是针对一些JPA注解的支持以及本项目中自定义的一些注解的支持,
MappingResolver.fixTableInfo(modelClass, tableInfo)
// 判断Mapper方法是否已经定义了sql声明,如果没有定义才进行注入,这样如果存在Mapper方法在xml文件中有定义则会优先使用,如果没有定义才会进行推断
val statementNames = this.configuration.mappedStatementNames
val unmappedFunctions = mapperClass.kotlin.declaredFunctions.filter {
(mapperClass.name + DOT + it.name) !in statementNames
}
// 解析未定义的方法,进行sql推断
val resolvedQueries = ResolvedQueries(mapperClass, unmappedFunctions)
unmappedFunctions.forEach { function ->
val resolvedQuery: ResolvedQuery = QueryResolver.resolve(function, tableInfo, modelClass, mapperClass)
resolvedQueries.add(resolvedQuery)
// query为null则表明推断失败,resolvedQuery中将包含推断失败的原因,会在后面进行统一输出,方便开发人员了解sql推断的具体结果和失败的具体原因
if (resolvedQuery.query != null && resolvedQuery.sql != null) {
val sql = resolvedQuery.sql
try {
val sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass)
when (resolvedQuery.type()) {
in listOf(QueryType.Select,
QueryType.Exists,
QueryType.Count) -> {
val returnType = resolvedQuery.returnType
var resultMap = resolvedQuery.resultMap
if (resultMap == null && resolvedQuery.type() == QueryType.Select) {
// 如果没有指定resultMap,则自动生成resultMap
val resultMapId = mapperClass.name + StringPool.DOT + function.name
resultMap = resolvedQuery.resolveResultMap(resultMapId, this.builderAssistant,
modelClass, resolvedQuery.query.mappings)
}
// addSelectMappedStatement这个方法中会使用默认的resultMap,该resultMap映射的类型和modelClass一致,所以如果当前方法的返回值和modelClass
// 不一致时,不能使用该方法,否则会产生类型转换错误
if (returnType == modelClass && resultMap == null) {
addSelectMappedStatement(mapperClass, function.name, sqlSource, returnType, tableInfo)
} else {
addMappedStatement(mapperClass, function.name,
sqlSource, SqlCommandType.SELECT, null, resultMap, returnType,
NoKeyGenerator(), null, null)
}
// 为select查询自动生成count的statement,用于分页时查询总数
if (resolvedQuery.type() == QueryType.Select) {
addSelectMappedStatement(mapperClass, function.name + COUNT_STATEMENT_SUFFIX,
languageDriver.createSqlSource(configuration, resolvedQuery.countSql(), modelClass),
Long::class.java, tableInfo
)
}
}
QueryType.Delete -> {
addDeleteMappedStatement(mapperClass, function.name, sqlSource)
}
QueryType.Insert -> {
// 如果id类型为自增,则将自增的id回填到插入的对象中
val keyGenerator = when {
tableInfo.idType == IdType.AUTO -> Jdbc3KeyGenerator.INSTANCE
else -> NoKeyGenerator.INSTANCE
}
addInsertMappedStatement(
mapperClass, modelClass, function.name, sqlSource,
keyGenerator, tableInfo.keyProperty, tableInfo.keyColumn
)
}
QueryType.Update -> {
addUpdateMappedStatement(mapperClass, modelClass, function.name, sqlSource)
}
else -> {
}
}
} catch (ex: Exception) {
LOG.error("""出错了 >>>>>>>>
可能存在下列情形之一:
${possibleErrors.joinToString { String.format("\n\t\t-\t%s\n", it) }}
""".trimIndent(), ex)
}
}
}
resolvedQueries.log()
// 其实这里的return是没有必要的,mybatis plus也没有对这个返回值做任何的处理,
// 所里这里随便返回了一个sql声明
return addSelectMappedStatement(mapperClass,
"unknown",
languageDriver.createSqlSource(configuration, "select 1", modelClass),
modelClass, tableInfo
)
}
Конкретно для разбора названий методов кодов очень много, и я не могу их здесь расписывать по порядку, поэтому говорю только об идее.Название метода не содержит всей информации необходимой для сборки sql, а всем по-прежнему нужна дополнительная информация, чтобы помочь. Эта информация в основном берется из аннотаций.
Аннотация, предоставляющая метаинформацию
@Хэндлер Аннотация относится к атрибуту постоянного класса, указывая на то, что атрибут необходимо преобразовать по типу, а значением аннотации является класс typeHandler mybatis.
@InsertIgnore Аннотация относится к атрибуту постоянного класса, указывающая, что атрибут не участвует в операции вставки в базу данных.
@UpdateIgnore Аннотация относится к свойству персистентного класса, указывающая, что свойство не участвует в операции обновления базы данных.
@SelectIgnore Аннотируется к свойству постоянного класса, указывающее, что свойство не участвует в операциях запросов к базе данных.
@ДжоинОбъект Указывает, что свойство является связанным сложным объектом, содержимое которого поступает из другой связанной таблицы базы данных.
@JoinProperty Указывает, что атрибут является ассоциативным атрибутом, а содержимое атрибута исходит из поля таблицы ассоциаций.
@ModifyIgnore Аннотация относится к свойству постоянного класса, указывающая, что свойство не участвует в операциях обновления базы данных и запросов.
@ResolvedName Аннотация относится к методу интерфейса Mapper, указывая на то, что вывод SQL использует имя, указанное в аннотации, вместо имени метода, поэтому имя метода не нужно менять для вывода SQL, что делает имя метода более логичным.
@SelectedProperties Аннотации методов интерфейса Mapper с указанием набора свойств постоянного объекта, используемого SQL-запросом, вставкой или обновлением.
@ValueAssign Используется для указания условия в @ResolvedName для использования определенного значения.
С помощью информации из приведенных выше аннотаций в сочетании с выводом имени метода можно автоматически вывести более 80% операций с базой данных.В простом сценарии приложения операция с базой данных может быть завершена без записи XML-файла, а следующее Чтобы присоединиться к конфигурации xml, это совершенно не влияет.
инструкции
Шаг 1: Добавьте репозиторий maven
<distributionManagement>
<repository>
<id>nexus</id>
<url>http://nexus.aegis-info.com/repository/maven-releases/</url>
</repository>
</distributionManagement>
Шаг 2: ссылка на зависимости в pom
<dependency>
<groupId>com.aegis</groupId>
<artifactId>aegis-starter-mybatis</artifactId>
<version>${mybatis-starter.version}</version>
</dependency>
Инструкции по настройке
Внедрение и использование этого проекта можно использовать без какой-либо настройки (конечно, необходима настройка mybatis)
То, требует ли интерфейс DAO, аннотированный @Mapper, вывод sql, является необязательным, а конфигурация файла xml сопоставления имеет более высокий приоритет.Если метод имеет конфигурацию в xml, вывод sql автоматически завершится ошибкой.
Использование этого плагина может быть постепенным.В начале использование этого плагина в проекте не влияет на исходный проект.Вы можете попробовать сначала удалить xml-конфигурацию некоторых методов, и пусть он использует вывод sql .Если работает нормально, то можно и дальше удалять xml, пока xml не дойдет до максимально упрощенного
включить вывод sql
Пусть интерфейс DAO с аннотацией @Mapper наследуетXmlLessMapperИнтерфейс может реализовать вывод sql DAO.
Интерфейс XmlLessMapper получает общий параметр, то есть объект, которым будет управлять DAO, и все выводы sql основаны на этом объекте.
Интерфейс XmlLessMapper не имеет методов по умолчанию и не повлияет на исходный код.
Выяснилось, что инъекция метода с помощью mybatis-plus должна наследовать интерфейс BaseMapper, но интерфейс BaseMapper имеет множество методов, большинство из которых может быть не нужно, поэтому я переписал эту логику, и метод по умолчанию не добавлен, пусть разработка добавляет DAO сам по себе требуемый метод,
Описание улучшения функции
Имя таблицы поддерживает аннотацию jpa __@Table__.Аннотация @TableName оригинального mybatis-plus по-прежнему действительна, но аннотация @Table имеет более высокий приоритет.
Атрибут первичного ключа поддерживает аннотацию jpa __@Id__
описание вывода sql
выбор вывода запроса
- Имена полей, полученные из имен методов, являются именами свойств объекта данных, связанного с картографом, а не именами полей таблицы в базе данных.
Пример 1 найти по идентификатору
решает
SELECT * FROM table WHERE id = #{id}
Пример 2 найти по имени
решает
SELECT * FROM table WHERE name = #{name}
Пример 3. findByNameLike
решает
SELECT * FROM table WHERE name LIKE CONCAT('%',#{name}, '%')
Пример 4. findByNameLikeKeyword
решает
SELECT * FROM table WHERE name LIKE CONCAT('%',#{keyword}, '%')
Пример 5. findByNameEqAndId
решает
SELECT * FROM table WHERE name = #{name} AND id = #{id}
Пример 6. findIdAndNameByAge
решает
SELECT id, name FROM table WHERE age = #{age}
предполагаемое имя sql изолировано от имени метода
Используйте аннотацию @ResolvedName в методе сопоставления. Требуемое имя параметра аннотации заменит имя метода в качестве имени предполагаемого sql, что может сделать имя метода более семантическим.
Например
@ResolvedName("findIdAndNameAndAge")
fun findSimpleInfoList(): List<User>
SQL будет выводиться с использованием findIdAndNameAndAge, и предполагаемый результат:
SELECT id,name,age FROM user
Набор свойств, полученных указанным методом
Аннотировать с помощью @SelectedProperties
Например
@SelectedProperties(properties=["id", "name", "age"])
fun findSimpleInfoList(): List<User>
@ResolvedName("findIdAndNameAndAge") в предыдущем примере можно заменить на @SelectedProperties(properties=["id", "name", "age"])
- Примечание. После аннотирования с помощью @SelectedProperties свойства запроса, выведенные из имени метода, будут игнорироваться.
удалить вывод операции
Поддержка метода записи deleteAll deleteById deleteByName
вывод операции обновления
Поддержка обновления объекта или обновления поля
Чтобы предотвратить ошибки обновления данных, операция обновления должна указывать свойство первичного ключа объекта.
пример 1:
fun update(user: User): Int
Окончательное решение:
UPDATE
user
SET
user.name = #{name},
user.password = #{password},
user.email = #{email}
WHERE
id = #{id}
Пример 2:
fun updateNameById(name:String,id:Int): Int
UPDATE
user
SET
user.name = #{name}
WHERE
id = #{id}
Поддержка операции вставки
Поддержка пакетной вставки
присоединиться к поддержке
присоединиться к объекту
С постоянным объектом может быть связан другой объект.Этот объект соответствует другой таблице в базе данных.Если вам нужно каскадировать запрос при запросе, вы можете настроить его следующим образом:
Добавьте аннотацию к свойству связанного объекта (поддержка одного объекта или набора объектов, то есть могут поддерживаться отношения «один к одному» или «один ко многим»):
@JoinObject(
targetTable = "t_score",
targetColumn = "student_id",
joinProperty = "id",
associationPrefix = "score_",
selectColumns = ["score", "subject_id"]
)
Атрибуты в аннотациях работают следующим образом: таблица targetTable для присоединения Имя столбца, используемого для ассоциации в таблице соединения targetColumn. joinProperty Имя свойства в текущем объекте, используемое для ассоциации (обратите внимание на имя свойства объекта, а не на имя столбца). AssociationPrefix Чтобы предотвратить конфликт имен столбцов, добавьте фиксированный префикс к псевдониму атрибута таблицы ассоциаций. selectColumns Набор столбцов для запроса в связанной таблице.
- Примечание. Если ассоциация представляет собой набор объектов, она должна быть объявлена как изменяемая коллекция в kotlin.
Поддержка весенних данных
Проект обеспечивает некоторую поддержку Spring Data, объекта Pageable, совместимого со Spring Data, в качестве параметра для разбиения по страницам и сортировки, а также поддерживает объект Page в качестве возврата для принятия данных подкачки и общего количества данных.
контрольная работа
построить таблицу данных
CREATE TABLE t_student
(
id VARCHAR(20) NOT NULL,
name VARCHAR(20) NOT NULL,
phone_number VARCHAR(20) NOT NULL,
sex INT NOT NULL,
CONSTRAINT t_student_id_uindex
UNIQUE (id)
);
ALTER TABLE t_student
ADD PRIMARY KEY (id);
CREATE TABLE t_score
(
id INT AUTO_INCREMENT
PRIMARY KEY,
score INT NOT NULL,
student_id VARCHAR(20) NOT NULL,
subject_id INT NOT NULL
);
CREATE TABLE t_subject
(
id INT AUTO_INCREMENT
PRIMARY KEY,
name VARCHAR(20) NOT NULL,
CONSTRAINT t_subject_name_uindex
UNIQUE (name)
);
создать объект данных
/**
*
* @author 吴昊
* @since 0.0.4
*/
class Student() {
@TableField("sex")
var gender: Int = 1
@Id
var id: String = ""
var name: String = ""
var phoneNumber: String = ""
@JoinObject(
targetTable = "t_score",
targetColumn = "student_id",
joinProperty = "id",
associationPrefix = "score_",
selectColumns = ["score", "subject_id"]
)
@ModifyIgnore
var scores: MutableList<Score>? = null
constructor(id: String, name: String, phoneNumber: String, gender: Int)
: this() {
this.id = id
this.name = name
this.phoneNumber = phoneNumber
this.gender = gender
}
}
class Score {
var score: Int = 0
var studentId: String = ""
var subjectId: Int = 0
}
Создать ДАО
@Mapper
interface UserDAO : XmlLessMapper<User> {
fun deleteById(id: Int)
@SelectedProperties(["name"])
fun findAllNames(): List<String>
fun findById(id: Int): User?
@ResolvedName("findById")
fun findSimpleUserById(id: Int): UserSimple
fun save(user: User)
fun saveAll(user: List<User>)
fun update(user: User)
fun count(): Int
}
написать тестовый класс
class StudentDAOTest : BaseTest() {
val id = "061251170"
@Autowired
private lateinit var studentDAO: StudentDAO
@Test
fun count() {
assert(studentDAO.count() > 0)
}
@Test
fun delete() {
val id = "061251171"
studentDAO.save(Student(
id,
"wuhao",
"18005184916", 1
))
assert(studentDAO.existsById(id))
studentDAO.deleteById(id)
assert(!studentDAO.existsById(id))
}
@Test
fun deleteByName() {
val id = "testDeleteByName"
val name = "nameOfTestDeleteByName"
studentDAO.save(
Student(
id,
name,
"18005184916", 1
)
)
assert(studentDAO.existsByName(name))
studentDAO.deleteByName(name)
assert(!studentDAO.existsByName(name))
}
@Test
fun existsByClientId() {
val id = "1234"
assert(!studentDAO.existsById(id))
}
@Test
fun findAll() {
val list = studentDAO.findAll()
val spec = list.first { it.id == id }
assert(spec.scores != null && spec.scores!!.isNotEmpty())
assert(list.isNotEmpty())
}
@Test
fun findById() {
val student = studentDAO.findById(id)
println(student?.scores)
assert(studentDAO.findById(id) != null)
}
@Test
fun findPage() {
studentDAO.findAllPageable(
PageRequest.of(0, 20)).apply {
this.content.map {
it.name + " / ${it.id}"
}.forEach { println(it) }
println(this.content.first().name.compareTo(this.content.last().name))
}
studentDAO.findAllPageable(
PageRequest.of(0, 20, Sort(Sort.Direction.DESC, "name"))).apply {
this.content.map {
it.name + " / ${it.id}"
}.forEach { println(it) }
println(this.content.first().name.compareTo(this.content.last().name))
}
studentDAO.findAllPageable(
PageRequest.of(0, 20, Sort.by("name"))).apply {
this.content.map {
it.name + " / ${it.id}"
}.forEach { println(it) }
println(this.content.first().name.compareTo(this.content.last().name))
}
}
@Test
fun save() {
studentDAO.deleteById(id)
assert(!studentDAO.existsById(id))
studentDAO.save(Student(
id,
"wuhao",
"18005184916", 1
))
assert(studentDAO.existsById(id))
}
@Test
fun saveAll() {
val id1 = "saveAll1"
val id2 = "saveAll2"
studentDAO.saveAll(
listOf(
Student(id1,
"zs", "123", 1),
Student(id2,
"zs", "123", 1)
)
)
assert(studentDAO.existsById(id1))
assert(studentDAO.existsById(id2))
studentDAO.deleteByIds(listOf("saveAll1", "saveAll2"))
assert(!studentDAO.existsById(id1))
assert(!studentDAO.existsById(id2))
}
@Test
fun selectPage() {
val page = studentDAO.findAllPage(PageRequest.of(0, 20))
println(page.content.size)
println(page.totalElements)
}
@Test
fun update() {
assert(
studentDAO.update(
Student(
"061251170", "zhangsan",
"17712345678",
9
)
) == 1
)
}
@Test
fun updateNameById() {
val id = "testUpdateNameById"
val oldName = "oldName"
val newName = "newName"
studentDAO.save(
Student(
id,
oldName,
"18005184916", 1
)
)
assert(studentDAO.findById(id)?.name == oldName)
assert(studentDAO.updateNameById(newName, id) == 1)
assert(studentDAO.findById(id)?.name == newName)
studentDAO.deleteById(id)
}
}
Результаты теста
напиши в конце
Проект был написан в спешке, на это ушло около недели, качество кода будет оптимизировано и улучшено на более позднем этапе, но на данный момент те функции, которые я хочу доделать, в основном выполнены.
Адрес проекта на Github:GitHub.com/five000/no…
Все желающие могут использовать и задавать вопросы и предложения