Вступление
Во время работы над аддоном для Jakarta-валидации мне пришлось писать логику по проверке изменений в модели по собственной аннотации CheckExistingByConstraintAndUnmodifiableAttributes.
Долго разглядывал получившейся код, и в голову пришла светлая (наверное) идея: почему бы не вынести все это в полноценный настраиваемый класс?
Для чего это решение
Как уже было сказано, решение предназначено для поиска и получения подробной информации о различиях (далее буду называть "дельтой") между двумя объектами.
Скажем, нам нужно проверить изменения по конкретным полям, которых может не быть в equals, и получить информацию о различиях отдельно для каждого поля. Допустим, как раз в рамках проверки определенных (не всех) полей на неизменяемость для моделек. И полной информации об ошибке, если изменения есть.
Вот в подобных кейсах мое решение - ChangeChecker - и можно использовать.
Поговорим о реализации идеи. Два объекта.
Я не буду сильно вдаваться в детали реализации (опять-таки, детали можно будет посмотреть в репозитории) и постараюсь сконцентрироваться на "спецификации".
ChangeChecker
Реализации этого интерфейса, собственно, и проделывают всю работу по поиску "дельты" между объектами. Про реализации - чуть позже, ну а пока выглядит он вот так.
Скрытый текст
/**
* Interface for finding differences between two objects.
* @param <T> - type of objects
* @see ValueChangesCheckerResult
*
* @author Ihar Smolka
*/
public interface ChangesChecker<T> {
/**
* Find differences between two objects.
* @param oldObj - old object
* @param newObj - new object
* @return finding result
*/
ValueChangesCheckerResult getResult(T oldObj, T newObj);
}
Все просто: на вход поступают два объекта одинакового типа, на выходе - получаем подробный результат сопоставления по двум объектам.
Как выглядит результат.
Скрытый текст
/**
* Result for check two objects.
* @see com.ismolka.validation.utils.change.ChangesChecker
*
* @param differenceMap - difference map
* @param equalsResult - equals result
* @author Ihar Smolka
*/
public record ValueChangesCheckerResult(
Map<String, Difference> differenceMap,
boolean equalsResult
) implements Difference, CheckerResult {
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ValueChangesCheckerResult that = (ValueChangesCheckerResult) o;
return equalsResult == that.equalsResult && Objects.equals(differenceMap, that.differenceMap);
}
@Override
public int hashCode() {
return Objects.hash(differenceMap, equalsResult);
}
@Override
public <T extends Difference> T unwrap(Class<T> type) {
if (type.isAssignableFrom(ValueChangesCheckerResult.class)) {
return type.cast(this);
}
throw new ClassCastException(String.format("Cannot unwrap ValueChangesCheckerResult to %s", type));
}
@Override
public CheckerResultNavigator navigator() {
return new DefaultCheckerResultNavigator(this);
}
}
И связанный интерфейс Difference.
Скрытый текст
/**
* Difference interface
*
* @author Ihar Smolka
*/
public interface Difference {
/**
* for unwrapping a difference
*
* @param type - toType
* @return unwrapped difference
* @param <TYPE> - type
*/
<TYPE extends Difference> TYPE unwrap(Class<TYPE> type);
}
Difference по смыслу близок к "интерфейсам-маркерам", т.к. он помечает все классы, касающиеся информации о "дельте". Если бы не метод unwrap, предназначенный для более "красивого" приведения Difference-объекта к конкретной реализации - можно было бы считать его таковым.
differenceMap - необходимо для хранения развернутой информации по различиям между двумя объектами. Здесь название поля/путь к полю маппится на определенный Difference. Это позволяет хранить сложную структуру "дельты" с вложениями самых разных видов (и результатам по Map, и по Collection, и прочее).
equalsResult - думаю, смысл понятен. Говорит, есть ли "дельта" у объектов.
ValueDifference
Выглядит так.
Скрытый текст
/**
* Difference between two values.
*
* @param valueFieldPath - attribute path from the root class.
* @param valueFieldRootClass - attribute root class.
* @param valueFieldDeclaringClass - attribute declaring class.
* @param valueClass - value class.
* @param oldValue - old value.
* @param newValue - new value.
* @param <F> - value type.
*
* @author Ihar Smolka
*/
public record ValueDifference<F>(String valueFieldPath,
Class<?> valueFieldRootClass,
Class<?> valueFieldDeclaringClass,
Class<F> valueClass,
F oldValue,
F newValue) implements Difference {
@Override
public <T extends Difference> T unwrap(Class<T> type) {
if (type.isAssignableFrom(ValueDifference.class)) {
return type.cast(this);
}
throw new ClassCastException(String.format("Cannot unwrap AttributeDifference to %s", type));
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ValueDifference<?> that = (ValueDifference<?>) o;
return Objects.equals(valueFieldPath, that.valueFieldPath) && Objects.equals(valueFieldRootClass, that.valueFieldRootClass) && Objects.equals(valueClass, that.valueClass) && Objects.equals(oldValue, that.oldValue) && Objects.equals(newValue, that.newValue);
}
@Override
public int hashCode() {
return Objects.hash(valueFieldPath, valueFieldRootClass, valueClass, oldValue, newValue);
}
}
Это класс для хранения базовой информации о двух различающихся объектах. Тут мы видим oldObject и newObject (смысл очевиден), их класс, а так же остальную мета-информацию, которая может оказаться полезной в рамках сопоставления объектов, как атрибутов определенного класса.
ValueCheckDescriptorBuilder
Основное содержимое такое.
Скрытый текст
/**
* Builder for {@link ValueCheckDescriptor}.
* @see ValueCheckDescriptor
*
* @param <Q> - value type
* @author Ihar Smolka
*/
public class ValueCheckDescriptorBuilder<Q> {
Class<?> sourceClass;
Class<Q> targetClass;
String attribute;
Set<String> equalsFields;
Method equalsMethodReflection;
BiPredicate<Q, Q> biEqualsMethod;
ChangesChecker<Q> changesChecker;
...
}
Служит для того, чтобы описывать, как именно будет проходить проверка двух атрибутов.
sourceClass - класс, в котором атрибут определен.
targetClass - класс атрибута.
attribute - название атрибута/путь.
equalsFields - внутренние поля для сопоставления по equals. Может работать совместно с установленным changesChecker, но с equalsMethodReflection и biEqualsMethod несовместимо.
equalsMethodReflection - экземпляр Method. Может пригодиться, когда передаем какой-то "кастомный equals" по рефлексии.
biEqualsMethod - BiPredicate, по которому будут сопоставляться объекты. Можно просунуть, например, Objects.equals (хотя это бессмысленно, т.к. Objects.equals вызовется в случае, если другие способы сопоставления не обозначены).
changesChecker - можно передавать для проверки какой-то вложенный ChangeChecker. Как это используется - можно будет понять по ходу статьи.
И ключевое.
DefaultValueChangesCheckerBuilder
Выглядит вот так и определяет настройки для проверки двух объектов.
Скрытый текст
/**
* Builder for {@link ValueCheckDescriptor}.
* @see DefaultValueChangesChecker
*
* @param <T> - value type
* @author Ihar Smolka
*/
public class DefaultValueChangesCheckerBuilder<T> {
Class<T> targetClass;
Set<ValueCheckDescriptor<?>> attributesCheckDescriptors;
boolean stopOnFirstDiff;
Method globalEqualsMethodReflection;
BiPredicate<T, T> globalBiEqualsMethod;
Set<String> globalEqualsFields;
...
}
targetClass - класс объектов.
attributesCheckDescriptors - описываются "сложные" чеки по атрибутам, используя предыдущий класс. Совместимо с globalEqualsFields, несовместимо с globalEqualsMethodReflection и globalBiEqualsMethod.
stopOnFirstDiff - останавливать ли проверку на первом различии.
globalEqualsFields - по каким атрибутам будет простой equals. По смыслу тоже самое, что и equalsFields в предыдущем классе, только работает уже "над" переданными ValueCheckDescriptor.
Примеры использования в виде тестов.
Скрытый текст
@Test
public void test_innerObject() {
ChangeTestObject oldTestObj = new ChangeTestObject();
ChangeTestObject newTestObj = new ChangeTestObject();
oldTestObj.setInnerObject(new ChangeTestInnerObject(OLD_VAL_STR));
newTestObj.setInnerObject(new ChangeTestInnerObject(NEW_VAL_STR));
CheckerResult result = DefaultValueChangesCheckerBuilder.builder(ChangeTestObject.class)
.addAttributeToCheck(
ValueCheckDescriptorBuilder.builder(ChangeTestObject.class, ChangeTestInnerObject.class)
.attribute("innerObject")
.addEqualsField("valueFromObject")
.build()
)
.build().getResult(oldTestObj, newTestObj);
ValueDifference<?> valueDifference = result.navigator().getDifference("innerObject.valueFromObject").unwrap(ValueDifference.class);
String oldValueFromCheckResult = (String) valueDifference.oldValue();
String newValueFromCheckResult = (String) valueDifference.newValue();
Assertions.assertEquals(oldValueFromCheckResult, oldTestObj.getInnerObject().getValueFromObject());
Assertions.assertEquals(newValueFromCheckResult, newTestObj.getInnerObject().getValueFromObject());
}
Скрытый текст
@Test
public void test_innerObjectWithoutValueDescriptor() {
ChangeTestObject oldTestObj = new ChangeTestObject();
ChangeTestObject newTestObj = new ChangeTestObject();
oldTestObj.setInnerObject(new ChangeTestInnerObject(OLD_VAL_STR));
newTestObj.setInnerObject(new ChangeTestInnerObject(NEW_VAL_STR));
CheckerResult result = DefaultValueChangesCheckerBuilder.builder(ChangeTestObject.class)
.addGlobalEqualsField("innerObject.valueFromObject")
.build().getResult(oldTestObj, newTestObj);
ValueDifference<?> valueDifference = result.navigator().getDifference("innerObject.valueFromObject").unwrap(ValueDifference.class);
String oldValueFromCheckResult = (String) valueDifference.oldValue();
String newValueFromCheckResult = (String) valueDifference.newValue();
Assertions.assertEquals(oldValueFromCheckResult, oldTestObj.getInnerObject().getValueFromObject());
Assertions.assertEquals(newValueFromCheckResult, newTestObj.getInnerObject().getValueFromObject());
}
Продолжаем разговор. Две коллекции/массива
CollectionChangesChecker
Для сравнения двух коллекций есть интерфейс CollectionChangesChecker, расширяющий базовый ChangesChecker.
Скрытый текст
/**
* Interface for check differences between two collections.
* @see CollectionChangesCheckerResult
*
* @param <T> - collection value type
*
* @author Ihar Smolka
*/
public interface CollectionChangesChecker<T> extends ChangesChecker<T> {
/**
* Find difference between two collections.
*
* @param oldCollection - old collection
* @param newCollection - new collection
* @return {@link CollectionChangesCheckerResult}
*/
CollectionChangesCheckerResult<T> getResult(Collection<T> oldCollection, Collection<T> newCollection);
/**
* Find difference between two arrays
*
* @param oldArray - old array
* @param newArray - new array
* @return {@link CollectionChangesCheckerResult}
*/
CollectionChangesCheckerResult<T> getResult(T[] oldArray, T[] newArray);
}
Как видим, появилось еще два метода - getResult по коллекциям и по массивам (в реализации массивы просто оборачиваются в List и проходят через getResult с коллекциями).
Возвращают они CollectionChangesCheckerResult.
CollectionChangesCheckerResult
Скрытый текст
/**
* Result for check two collections.
*
* @param collectionClass - collection value class.
* @param collectionDifferenceMap - collection difference.
* @param equalsResult - equals result
* @param <F> - type of collection values
*
* @author Ihar Smolka
*/
public record CollectionChangesCheckerResult<F>(
Class<F> collectionClass,
Map<CollectionOperation, Set<CollectionElementDifference<F>>> collectionDifferenceMap,
boolean equalsResult) implements Difference, CheckerResult {
...
}
Скрытый текст
/**
* Possible modifying operations for {@link java.util.Collection}.
*
* @author Ihar Smolka
*/
public enum CollectionOperation {
/**
* Add element
*/
ADD,
/**
* Remove element
*/
REMOVE,
/**
* Update element
*/
UPDATE
}
Скрытый текст
/**
* Difference between two elements of {@link java.util.Collection}.
*
* @param diffBetweenElementsFields - difference between elements.
* @param elementFromOldCollection - element from old collection.
* @param elementFromNewCollection - element from new collection.
* @param elementFromOldCollectionIndex - index of element from old collection.
* @param elementFromNewCollectionIndex - index of element from new collection.
* @param <F> - type of collection elements.
*
* @author Ihar Smolka
*/
public record CollectionElementDifference<F>(
Map<String, Difference> diffBetweenElementsFields,
F elementFromOldCollection,
F elementFromNewCollection,
Integer elementFromOldCollectionIndex,
Integer elementFromNewCollectionIndex
) implements Difference {
...
}
Как видим, в этот раз хранящая "дельту" информация представлена в виде мапы, в которой операция по изменению коллекции сопоставлена с множеством изменений этого типа.
Ну а CollectionElementDifference содержит информацию про то, какие элементы из каких коллекций различаются, на каких индексах и какие именно между ними различия. Для операции UPDATE оба элемента должны быть заполнены. Для ADD будет отсутствовать старый элемент, для REMOVE - соответственно, новый.
DefaultCollectionChangesCheckerBuilder
Скрытый текст
**
* Builder for {@link DefaultCollectionChangesChecker}
*
* @param <T> - type of collection elements.
*
* @author Ihar Smolka
*/
public class DefaultCollectionChangesCheckerBuilder<T> {
Class<T> collectionGenericClass;
Set<ValueCheckDescriptor<?>> attributesCheckDescriptors;
boolean stopOnFirstDiff;
Set<CollectionOperation> forOperations;
Set<String> fieldsForMatching;
Method globalEqualsMethodReflection;
BiPredicate<T, T> globalBiEqualsMethod;
Set<String> globalEqualsFields;
...
}
В принципе, все почти аналогично DefaultValueChangesCheckerBuilder, поговорим о различиях.
fieldsForMatching - по каким полям будут сопоставляться объекты в рамках коллекций. Т.е., если эти поля у двух элементов в разных коллекциях совпадают - то они будут сопоставляться друг с другом, и если "дельта" между ними есть - тогда это UPDATE элемента в коллекции. Если это не определено - в качестве такого "ключа" будет выступать индекс в коллекции.
forOperations - для каких операций мы получаем "дельту". По умолчанию для всех.
collectionGenericClass - экземпляры какого класса коллекция в себе держит.
Пример использования в виде теста.
Скрытый текст
@Test
public void test_collection() {
String key = "ID_IN_COLLECTION";
ChangeTestObject oldTestObj = new ChangeTestObject();
ChangeTestObject newTestObj = new ChangeTestObject();
ChangeTestObjectCollection oldCollectionObj = new ChangeTestObjectCollection(key, OLD_VAL_STR);
ChangeTestObjectCollection newCollectionObj = new ChangeTestObjectCollection(key, NEW_VAL_STR);
oldTestObj.setCollection(List.of(oldCollectionObj));
newTestObj.setCollection(List.of(newCollectionObj));
CheckerResult result = DefaultValueChangesCheckerBuilder.builder(ChangeTestObject.class)
.addAttributeToCheck(
ValueCheckDescriptorBuilder.builder(ChangeTestObject.class, ChangeTestObjectCollection.class)
.attribute("collection")
.changesChecker(
DefaultCollectionChangesCheckerBuilder.builder(ChangeTestObjectCollection.class)
.addGlobalEqualsField("valueFromCollection")
.addFieldForMatching("key")
.build()
).build()
).build().getResult(oldTestObj, newTestObj);
CollectionElementDifference<ChangeTestObjectCollection> difference = result.navigator().getDifferenceForCollection("collection", ChangeTestObjectCollection.class).stream().findFirst().orElseThrow(() -> new RuntimeException("Result for collection is not present"));
Assertions.assertEquals(difference.elementFromOldCollection().getValueFromCollection(), oldCollectionObj.getValueFromCollection());
Assertions.assertEquals(difference.elementFromNewCollection().getValueFromCollection(), newCollectionObj.getValueFromCollection());
}
Разговор приближается к концу. Две мапы
MapChangesChecker
На то у нас есть следующий интерфейс.
Скрытый текст
/**
* Interface for check differences between two maps.
* @see MapChangesCheckerResult
*
* @param <K> - key type
* @param <V> - value type
*
* @author Ihar Smolka
*/
public interface MapChangesChecker<K, V> extends ChangesChecker<V> {
/**
* Find difference between two maps.
*
* @param oldMap - old map
* @param newMap - new map
* @return difference result
*/
MapChangesCheckerResult<K, V> getResult(Map<K, V> oldMap, Map<K, V> newMap);
}
K описывает класс ключа, V - соответственно, класс значения для мап.
MapChangesCheckerResult
Скрытый текст
/**
* Result for check two maps.
* @see MapElementDifference
*
* @param keyClass - key class
* @param valueClass - value class
* @param mapDifference - map difference
* @param equalsResult - equals result
* @param <K> - key type
* @param <V> - value type
*
* @author Ihar Smolka
*/
public record MapChangesCheckerResult<K, V>(
Class<K> keyClass,
Class<V> valueClass,
Map<MapOperation, Set<MapElementDifference<K, V>>> mapDifference,
boolean equalsResult
) implements Difference, CheckerResult {
...
}
Скрытый текст
/**
* Possible modifying operations for {@link java.util.Map}.
*
* @author Ihar Smolka
*/
public enum MapOperation {
/**
* Add element
*/
PUT,
/**
* Remove element
*/
REMOVE,
/**
* Update element
*/
UPDATE
}
Скрытый текст
/**
* Difference between two elements of {@link Map}.
*
* @param diffBetweenElementsFields - difference between elements
* @param elementFromOldMap - element from the old map
* @param elementFromNewMap - element from tht new map
* @param key - map key with difference
* @param <K> - key type
* @param <V> - value type
*
* @author Ihar Smolka
*/
public record MapElementDifference<K, V>(
Map<String, Difference> diffBetweenElementsFields,
V elementFromOldMap,
V elementFromNewMap,
K key
) implements Difference {
...
}
В целом, похоже на CollectionChangesCheckerResult, только теперь здесь присутствуют классы ключа и значения. Ну и мапа с "дельтой" держит чуть другую информацию - подробно останавливаться на ней вряд ли имеет смысл, все должно быть понятно уже без лишних слов.
DefaultMapChangesCheckerBuilder
Скрытый текст
/**
* Builder for {@link DefaultMapChangesChecker}
*
* @param <K> - key type
* @param <V> - value type
*/
public class DefaultMapChangesCheckerBuilder<K, V> {
Class<K> keyClass;
Class<V> valueClass;
Set<MapOperation> forOperations;
Set<ValueCheckDescriptor<?>> attributesCheckDescriptors;
boolean stopOnFirstDiff;
Method globalEqualsMethodReflection;
BiPredicate<V, V> globalBiEqualsMethod;
Set<String> globalEqualsFields;
...
}
Опять-таки, думаю, здесь все понятно без лишних слов, т.к. очень похоже на предыдущие билдеры.
По традиции.
Скрытый текст
@Test
public void test_map() {
String key = "ID_IN_MAP";
ChangeTestObject oldTestObj = new ChangeTestObject();
ChangeTestObject newTestObj = new ChangeTestObject();
ChangeTestObjectMap oldMapObj = new ChangeTestObjectMap(OLD_VAL_STR);
ChangeTestObjectMap newMapObj = new ChangeTestObjectMap(NEW_VAL_STR);
oldTestObj.setMap(Map.of(key, oldMapObj));
newTestObj.setMap(Map.of(key, newMapObj));
CheckerResult result = DefaultValueChangesCheckerBuilder.builder(ChangeTestObject.class)
.addAttributeToCheck(
ValueCheckDescriptorBuilder.builder(ChangeTestObject.class, ChangeTestObjectMap.class)
.attribute("map")
.changesChecker(
DefaultMapChangesCheckerBuilder.builder(String.class, ChangeTestObjectMap.class)
.addGlobalEqualsField("valueFromMap")
.build()
).build()
).build().getResult(oldTestObj, newTestObj);
MapElementDifference<String, ChangeTestObjectMap> difference = result.navigator().getDifferenceForMap("map", String.class, ChangeTestObjectMap.class).stream().findFirst().orElseThrow(() -> new RuntimeException("Result for map is not present"));
Assertions.assertEquals(difference.elementFromOldMap().getValueFromMap(), oldMapObj.getValueFromMap());
Assertions.assertEquals(difference.elementFromNewMap().getValueFromMap(), newMapObj.getValueFromMap());
}
Разговор почти окончен. Навигация по результату
На мой взгляд, пользуясь этими инструментами можно относительно легко получить "дельту" по объектами любой (ну или практически любой) структуры.
Вопрос теперь в том, как нам удобно «навигировать» по полученному бардаку полученной дельте. На помощь приходит следующий интерфейс.
Скрытый текст
/**
* Interface for navigation in {@link com.ismolka.validation.utils.change.CheckerResult}.
* @see com.ismolka.validation.utils.change.CheckerResult
*
* @author Ihar Smolka
*/
public interface CheckerResultNavigator {
/**
* Get difference for {@link java.util.Map}
*
* @param fieldPath - attribute path with difference.
* @param keyClass - key class.
* @param valueClass - value class.
* @param operations - return for {@link MapOperation}.
* @return {@link Set} of {@link MapElementDifference} - if differences are there and 'null' - if aren't.
* @param <K> - key type.
* @param <V> - value type.
*/
<K, V> Set<MapElementDifference<K, V>> getDifferenceForMap(String fieldPath, Class<K> keyClass, Class<V> valueClass, MapOperation... operations);
/**
* Get difference for {@link java.util.Collection}
*
* @param fieldPath - attribute path with difference.
* @param forClass - class of collection values.
* @param operations - return for {@link CollectionOperation}.
* @return {@link Set} of {@link CollectionElementDifference} - if differences are there and 'null' - if aren't.
* @param <T> - value type
*/
<T> Set<CollectionElementDifference<T>> getDifferenceForCollection(String fieldPath, Class<T> forClass, CollectionOperation... operations);
/**
* Get difference for {@link java.util.Map}
*
* @param keyClass - key class.
* @param valueClass - value class.
* @param operations - return for {@link MapOperation}.
* @return {@link Set} of {@link MapElementDifference} - if differences are there and 'null' - if aren't.
* @param <K> - key type.
* @param <V> - value type.
*/
<K, V> Set<MapElementDifference<K, V>> getDifferenceForMap(Class<K> keyClass, Class<V> valueClass, MapOperation... operations);
/**
* Get difference for {@link java.util.Collection}
*
* @param forClass - class of collection values.
* @param operations - return for {@link CollectionOperation}.
* @return {@link Set} of {@link CollectionElementDifference} - if differences are there and 'null' - if aren't.
* @param <T> - value type
*/
<T> Set<CollectionElementDifference<T>> getDifferenceForCollection(Class<T> forClass, CollectionOperation... operations);
/**
* Get difference for attribute.
*
* @param fieldPath - attribute path with difference.
* @return {@link Difference} - if differences are there and 'null' - if aren't.
*/
Difference getDifference(String fieldPath);
/**
* Get difference.
*
* @return {@link Difference}
*/
Difference getDifference();
}
И каждый класс результата проверки отдаст нам по методу navigator() дефолтную реализацию этого интерфейса.
Через навигатор мы можем продираться через множество вложений и получать интересующую нас "дельту". Ну или null, если таковой не найдено.
Для "распаковки дельт" из коллекций и мап нужно использовать соответствующие методы getDifferenceForMap и getDifferenceForCollection (если интересует конкретная операция/операции - передаем в конце методов).
При навигации следует учитывать, что если, скажем, где-то на середине нашего "пути" будет какая-то коллекция или мапа - навигатор вернет ошибку. Подобное должно быть только в конце пути. Поэтому когда нам, скажем, надо получить "коллекцию в коллекции" - получаем "дельты" первой коллекции, дальше дергаем навигаторы уже у этих "дельт".
Как все это выглядит - можно увидеть по примерам в тестах.
Конец разговора
С решением можно ознакомиться во все том же репозитории с аддоном для валидации, в пакете com.ismolka.validation.utils.change.
Код еще не до конца приведен в божеский вид и, скорее всего, еще будет мелкий рефакторинг (как минимум).
Интересует ваше мнение. Насколько нужная штука, насколько хорошее решение, замечания и рекомендации по коду тоже приветствуются.
Всем дзякую!
Комментарии (15)
LeshaRB
02.09.2024 19:50+2У меня была задача записывать аудит изменений в БД
Выбор был между Javers и EnversJavers как-то резво развивался, плюс автор отвечал
Плюс поддержка монгиВыбрал его
igorsmolkako Автор
02.09.2024 19:50Раз вы пользовались... как по вашему, отличается функциональность моего "велосипеда" от Javers? И в какую сторону, если есть отличия, в лучшую или худшую?)
LeshaRB
02.09.2024 19:50Javers имеет 1.4k звёзд на гитхабе
И несколько лет развитияСогласитесь глупый вопрос
igorsmolkako Автор
02.09.2024 19:50Это понятно, что масштабы несопоставимы. Это вообще полноценный фреймворк (а не написанная за неделю либа), как я понимаю, в котором куча вещей помимо чекинга изменений.
Но если только в разрезе функции проверки изменений и использования? Ограничиваясь чисто этим узким функционалом.
LeshaRB
02.09.2024 19:50+2Понимаете, то что вы решили написать сами что-то своё.это вам плюс.
Но то что изобрели велосипед не промониторив мир, это минусНапример вы прочитали книгу старую книгу Beginning Java EE 7
И решите писать свое. DIСогласитесь будет смешно.
Рядом гиганты spring, quarkus, micronaut
И куча light библиотекКак нет проект , попробовать, я согласен, опыт
Но не для прода.Тем более ушли времена надеюсь, когда для Джаспера ,мне приходилось подключать itext монстра)
igorsmolkako Автор
02.09.2024 19:50+1Не, ну промониторить-то я пытался, только на глаза тот же Javers так и не попался) Видать, плохо упрашивал Гугл)
А так был искренне убежден, что делаю что-то новое, хоть и пока "на коленке") Эх, суровая реальность)
Cейчас хочу определиться с возможным дальнейшим планом развития. Может, есть все же что-то, какая-то важная мелочь/мелочи, которых не хватает в готовых решениях подобного плана (несмотря на их бОльший масштаб - порой у разрабов до мелочей не доходят руки, даже до важных :)) и которые у меня имеются/которые буду готов реализовать. Например, в том же Javers, видимо, для "дельты" по коллекциями какое-то время нельзя было получить старый и новый объекты (https://github.com/javers/javers/issues/1094). Что-то в этом роде.
Если их добавить - то как такая "light"-либа, думаю, будет вполне себе и даже в мавен централ залить будет не стыдно.
bulatenkom
02.09.2024 19:50+1Вместо гугла, уже есть более современные средства поиска. Тот же чатгпт, сходу выдал несколько готовых решений:
Java Object Diff (https://github.com/SQiShER/java-object-diff)
Gson/JsonDiff
Javers
Apache Commons Lang (EqualsBuilder и DiffBuilder)
Помимо, собственно поиска, можно закинуть такому чату ваше решение и попросить его сравнить, найти отличия, составить отчёт/сводку.
igorsmolkako Автор
02.09.2024 19:50+1Пока все руки не доходили до этого чуда)
Первое вроде видел, но мне как-то не очень понравилось. Второе, судя по всему, не совсем про Java-объекты. Про третье уже говорили, четвертое тоже выглядит интересно, нужно будет ознакомиться. Спасибо)
moonster
02.09.2024 19:50+3Ну вот. ) Хотел поделиться аналогичным опытом, но смысла уже нет - вы меня опередили. И это хорошо. Тот самый код я не имею возможности опубликовать ни по техническим, ни по юридическим причинам, мне бы пришлось его пересочинить.
Десять лет назад я создал очень похожий инструмент. Javers тогда уже существовал, но его версия была меньше чем 1.0.0, то есть не production ready.
Изначально назначение у инструмента было другое - обеспечение конкурентной работы.
Объект не сохранялся в БД напрямую. Вместо этого под распределенной блокировкой вычислялся дифф между исходной и измененной версией объекта, и далее дифф применялся к актуальной версии объекта, которая и сохранялась в БД. Сохранялись в БД и диффы, которые в дальнейшем можно было использовать при расследовании разного рода инцидентов, а так же для восстановления исторических версий объекта, которые могли быть нужны для разного рода миграций.
Таким образом, несколько юзеров могли одновременно отредактировать разные поля, одновременно сохранить объект, и ничьи изменения не терялись в результате гонки.
Опытный разработчик может возразить, что правильный способ обеспечения конкурентной работы - это команды, а не вот это вот всё поверх CRUD'а, и я с ним соглашусь.
Sigest
02.09.2024 19:50+2Наверное каждый разработчик в своей жизни пишет велосипед, а потом…хоба…и обнаруживает на рынке что-то готовое, намного функциональнее. У меня тоже так было. Писал в свое время для себя аналог Jooq или QueryDSL, но как потом оказалось эти инструменты уже существовали. Ну и ладно, сказал я себе. Зато хороший опыт написания фреймворка с нуля. Выбрал я, естественно, готовые решения
Iceberk
Это не то же самое что Javers? https://javers.org/
igorsmolkako Автор
Похоже)
Чорт, не знал про эту штуку.
Оказывается, велосипед или близко к этому... Печаль)
Но по крайней мере попытка интересная вышла. Да и, мб, функциональность местами все же будет различаться. Не знаком c Javers, пускай уж знатоки рассудят.