Русский
Русский
English
Статистика
Реклама

Repository

Перевод Работаем с NPM реестром из Java

29.07.2020 08:08:02 | Автор: admin
image
NPM уникальный репозиторий пакетов из мира JavaScript. В основном здесь те JS библиотеки, которые можно использовать во фронтэнде/в браузере, но есть и серверные для использования в node.js и не только. Если вы программируете на Java и у вас появилась необходимость синтегрироваться с NPM репозиторием, то скорее всего у вас один из двух следующих случаев:
  • Вы пишите Web приложение на одном из Java фреймворков и определенные NPM пакеты необходимы для клиентской стороны
  • У вас Java приложение (например, для Андройда), которому необходимо уметь запрашивать зависимости и сами ресурсы/пакеты из NPM

Давайте посмотрим как это можно сделать в Java.



NPM ресурсы для Web-приложения


У вас есть 2 опции:
  • Запаковать нужные NPM ресурсы внутрь вашего WAR/JAR
  • Использовать CDN для загрузки в runtime нужных ресурсов


Упаковка NPM ресурсов в WAR/JAR


Прежде всего вам надо узнать побольше о такой штуке как WebJars. Она позволяет отражать NPM (и не только) пакеты в репозиторий Maven. Таким образом вы можете работать с NPM пакетами как с обычными Java пакетами в Maven. Например, для того чтобы включить ресурсы из всем известного Boostrap в ваш WAR достаточно добавить следующую зависимость в pom.xml:

<dependency>    <groupId>org.webjars.npm</groupId>    <artifactId>bootstrap</artifactId>    <version>4.5.0</version></dependency>


WebJars отражает пакеты из NPM в Maven вместе со всеми необходимыми зависимостями, так что подключив один JAR по зависимостям будут подключены и все остальные нужные пакеты.
WebJars так же имеет большой набор библиотек под разные Java фреймворки для того чтобы удобнее было работать с запакованными и подключенными ресурсами. Подробнее читайте в документации.

WebJars отличное подспорье для любого Java Backend разработчика. Но есть и более легкие альтернативы: упаковка нужных пакетов из NPM с помощью Maven плагинов. Здесь, возможно, не полный список:

Например, чтобы включить пакеты vue и vuex нужных версий с помощью jnpm-maven-plugin добавьте следующие строчки в pom.xml:

<plugin>    <groupId>org.orienteer.jnpm</groupId>    <artifactId>jnpm-maven-plugin</artifactId>    <version>1.0</version><executions><execution><goals><goal>install</goal></goals><configuration><packages><package>vue@2.6.11</package><package>vuex@~3.4.0</package></packages></configuration></execution></executions></plugin>


Можно использовать нотацию NPM для определения диапазона нужных версий:

  • Звездочка (*|X|x) 1.* эквивалентно >=1.0.0 & <2.0.0
  • Тильда (~) ~1.5 эквивалентно >=1.5.0 & <1.6.0
  • Дефис (-) 1.0-2.0 эквивалентно >=1.0.0 & <=2.0.0
  • Каретка (^) ^0.2.3 эквивалентно >=0.2.3 & <0.3.0
  • Частичный диапазон 1 эквивалентно 1.X or >=1.0.0 & <2.0.0
  • Отрицание !(1.x) эквивалентно <1.0.0 & >=2.0.0
  • Сложные ~1.3 | (1.4.* & !=1.4.5) | ~2


Также, вы можете задать какие именно файлы нужно включить из пакетов используя includes и excludes. Например, обычно NPM пакет содержит скомпиленные файлы в директории /dist. Другие файлы являются исходниками и врядли будут нужны и полезны внутри Java Web приложения. Чтобы включить только содержимое директории dist/ достаточно добавить следующее в секцию :

<includes>  <include>dist/*</include></includes>


По умолчанию jnpm-maven-plugin запаковывает ресурсы по точно таким же путям как и WebJars. Это позволяет использовать упомянутые выше библиотеки WebJars для разных фреймворков для доступа к ресурсам. Если вам необходим какой-то другой специфичный формат упаковывания, то просьба обратиться к документации.

Использование CDN


Есть множество публично доступных CDN с NPM ресурсами. Наиболее известные и используемые:


Также вы можете использовать свой собственный CDN (например поднятый через docker) или даже встроить CDN функционал внутрь вашего Web-App. Например, добавьте следующий сервлет в web.xml чтобы включить JNPM CDN. Отредактируйте по необходимости:

<servlet>  <servlet-name>CDNServlet</servlet-name>  <servlet-class>org.orienteer.jnpm.cdn.CDNServlet</servlet-class></servlet><servlet-mapping>  <servlet-name>CDNServlet</servlet-name>  <url-pattern>/cdn/*</url-pattern></servlet-mapping>

После загрузки сервлета NPM ресурсы будут доступны через URL следующего формата: http(s)://<домен>:<порт>/<путь до веб приложения>/cdn/<NPM пакет>/<путь до файла>.
Например:
localhost:8080/cdn/vue@2.6.11/dist/vue.js


Работа с NPM REST API из Java


Безусловно, вы можете использовать напрямую REST API реестра NPM, скажем, через Retrofit. В этом вам поможет соответствующая документация. Но удобнее использовать библиотеку JNPM, которая представляет Java обертку для данного REST API и не только.
Включаем JNPM Jar в pom.xml:
<dependency>    <groupId>org.orienteer.jnpm</groupId>    <artifactId>jnpm</artifactId>    <version>1.0</version></dependency>


Инициализируем JNPM API:
JNPMService.configure(JNPMSettings.builder()  .homeDirectory(Paths.get("/home/myuser/.jnpm")) //Опционально  .downloadDirectory(Paths.get("/tmp")) //Опционально  //Другие возможные опции - см. документацию .build());


JNPM API предоставляет 2 опции: Синхронное API и Ассинхронное API через RXJava. Что именно использовать решать вам:

JNPMService jnpmService = JNPMService.instance(); //Synchronous Java APIRxJNPMService rxJnpmService = JNPMService.instance().getRxService() //RXJava API


Пример использования:

//Общая информаци о NPM реестреSystem.out.println(JNPMService.instance().getRegistryInfo());//Заполучить и напечатать информацию о последней версии VUESystem.out.println(JNPMService.instance().getPackageInfo("vue").getLatest());//Напечатать описание пакета vue@2.6.11System.out.println(JNPMService.instance().getVersionInfo("vue", "2.6.11").getDescription());//Напечатать последнюю версию до второго официального релизаSystem.out.println(JNPMService.instance().bestMatch("vue@<2").getVersionAsString());//Скачать архив для vue@2.6.11 и напечатать локальный путьVersionInfo vueVersion = JNPMService.instance().getVersionInfo("vue", "2.6.11");vueVersion.downloadTarball().blockingAwait();System.out.println(vueVersion.getLocalTarball().getAbsolutePath());//Искать "vue" и напечатать описание первого результатаSystem.out.println(JNPMService.instance().search("vue").getObjects().get(0).getSearchPackage().getDescription());// Пройтись и напечатать информацию по всем dev зависимостям последней версии vue // а так же установить данные пакеты так как делает это сам NPM (node_modules/vue и т.д.)JNPMService.instance().getRxService()   .traverse(TraverseDirection.WIDER, TraversalRule.DEV_DEPENDENCIES, "vue")   .subscribe(t -> {System.out.println(t); t.install(Paths.get("target", "readme"), InstallationStrategy.NPM);});


Если у вас какой-то специфичный случай, который был не описан здесь пожалуйста, дайте знать!
Подробнее..
Категории: Node.js , Java , Api , Npm , Repository

Core Data Repository pattern. Детали реализации

10.05.2021 20:18:52 | Автор: admin

Всем привет! Данный пост рассчитан на людей, которые имеют представление о Core Data. Если вы не один из них, прочитайте краткую информацию тут и скорее присоединяйтесь. Прежде всего, мне хотелось бы поделиться своим взглядом на некоторые подходы к организации работы с данными в IOS приложениях, и в этом вопросе мне пришелся по душе паттерн репозиторий (далее - репозиторий). Поехали!

Немного про репозиторий

Основная идея репозитория заключается в том, чтобы абстрагироваться от источников данных, скрыть детали реализации, ведь, c точки зрения доступа к данным, эта самая реализация - не играет роли. Предоставить доступ к основным операциям над данными: сохранить, загрузить или удалить - главная задача репозитория.

Таким образом, к его преимуществам можно отнести:

- отсутствие зависимостей от реализации репозитория. Под капотом может быть все, что угодно: коллекция в оперативной памяти, UserDefaults, KeyChain, Core Data, Realm, URLCache, отдельный файл в tmp и т.п.;

- разделение зон ответственности. Репозиторий выступает прослойкой между бизнес-логикой и способом хранения данных, отделяя одно от другого;

- формирование единого, более структурированного подхода в работе с данными.

В конечном итоге, все это благоприятно сказывается на скорости разработки, возможностях масштабирования и тестируемости проектов.

Ближе к деталям

Рассмотрим самый неблагоприятный сценарий использования Core Data.

Для общего понимания будем использовать класс DataProvider (далее - DP) - его задача получать данные откуда - либо (сеть, UI) и класть их в Repository . Также, при необходимости, DP может достать данные из репозитория, выступая для него фасадом. Под данными будем подразумевать массив доменных объектов. Именно ими оперирует репозиторий.

1. Core Data как один большой репозиторий с NSManagamentObject

Рисунок 1Рисунок 1

Идея оперировать NSManagedObject в качестве доменного объекта самая простая, но не самая удачная. При таком подходе перед нами встают сразу несколько проблем:

  1. Сore Data разрастается по всему проекту, вешая ненужные зависимости и нарушая зоны ответственности. Раскрываются детали реализации. Знать на чем завязана реализация репозитория - должен только репозиторий;

  2. Используя единый репозиторий для всех Data Provider, он будет разрастаться с появлением новых доменных объектов;

  3. В худшем случае, логика работы с объектами начнет пересекаться между собой и это может превратиться в один большой непредсказуемый magic.

2. Core Data + DB Client

Первое, что приходит на ум, для решения проблем из предыдущего примера, это вынести логику работы с объектами в отдельный класс (назовем его DB Client), тогда наш Repository будет только сохранять и доставать объекты из хранилища, в то время, как вся логика по работе с объектами ляжет в DB Client. На выходе должно получиться что-то такое:

Рисунок 2Рисунок 2

Обе схемы решают проблему 1. (Core Data ограничивается DB Client и Repository), и частично могут решить проблему 2 и 3 на небольших проектах, но не исключают их полностью. Продолжая мысль дальше, возможно придти к следующей схеме:

Рисунок 3Рисунок 3
  1. Core Data можно ограничить только репозиторием. DB Client конвертирует доменные объекты в NSManagedObject и обратно;

  2. Repository больше не единый и он не разрастается;

  3. Логика работы с данными более структурирована и консолидирована

Здесь, стоит отметить, что вариантов композиции и декомпозиции классов, а также способов организации взаимодействия между ними - множество. Тем не менее, выше-описанная схема показывает еще одну, на мой взгляд, важную проблему: на каждый новый доменный объект, в лучшем случае, требуется заводить X2 объектов (DB Client и Repository). Поэтому, предлагаю рассмотреть еще один способ реализации. Для удобства, проект лежит тут.

Подготовка к реализации

В первую очередь, таким я вижу репозиторий:

protocol AccessableRepository {   //1   associatedtype DomainModel   //2   var actualSearchedData: Observable<[DomainModel]>? {get}      //3   func save(_ objects: [DomainModel], completion: @escaping ((Result<Void>) -> Void))   //4   func save(_ objects: [DomainModel], clearBeforeSaving: RepositorySearchRequest, completion: @escaping ((Result<Void>) -> Void))      //5   func present(by request: RepositorySearchRequest, completion: @escaping ((Result<[DomainModel]>) -> Void))      //6   func delete(by request: RepositorySearchRequest, completion: @escaping ((Result<Void>) -> Void))   //7   func eraseAllData(completion: @escaping ((Result<Void>) -> Void))}
  1. Доменный объект, которым оперирует репозиторий;

  2. Возможность подписаться на отслеживание изменений в репозитории;

  3. Сохранение объектов в репозиторий;

  4. Сохранение объектов с возможностью очистки старых данных в рамках одного контекста;

  5. Загрузка данных из репозитория;

  6. Удаление объектов из репозитория;

  7. Удаление всех данных из репозитория.

Возможно, ваш набор требований к репозиторияю будет отличаться, но концептуально ситуацию это не изменит.

К сожалению, возможность работать с репозиторием через AccessableRepository отсутствует, о чем свидетельствует ошибка на рисунке 4:

Рисунок 4Рисунок 4

В таком случае, хорошо подходит Generic-реализация репозитория, которая выглядит следующим образом:

class Repository<DomainModel>: NSObject, AccessableRepository {   typealias DomainModel = DomainModel      var actualSearchedData: Observable<[DomainModel]>? {      fatalError("actualSearchedData must be overrided")   }      func save(_ objects: [DomainModel], completion: @escaping ((Result<Void>) -> Void)) {      fatalError("save(_ objects: must be overrided")   }   func save(_ objects: [DomainModel], clearBeforeSaving: RepositorySearchRequest, completion: @escaping ((Result<Void>) -> Void)) {      fatalError("save(_ objects vs clearBeforeSaving: must be overrided")   }      func present(by request: RepositorySearchRequest, completion: @escaping ((Result<[DomainModel]>) -> Void)) {      fatalError("present(by request: must be overrided")   }      func delete(by request: RepositorySearchRequest, completion: @escaping ((Result<Void>) -> Void)) {      fatalError("delete(by request: must be overrided")   }   func eraseAllData(completion: @escaping ((Result<Void>) -> Void)) {      fatalError("eraseAllData(completion: must be overrided")   }}
  1. NSObject нужен для взаимодействия с NSFetchResultController;

  2. AccessableRepository - для наглядности, прозрачности и порядка;

  3. FatalError играет роль предохранителя, чтобы всяк сюда входящий не использовал то, что не реализовано;

  4. Данное решение позволяет не привязываться к конкретной реализации, а также обойти предыдущую проблему:

Рисунок 5Рисунок 5

Для работы с выборкой объектов потребуются объект с двумя свойствами:

protocol RepositorySearchRequest {   /* NSPredicate = nil,  apply for all records    for deletion sortDescriptor is not Used    */   //1   var predicate: NSPredicate? {get}   //2   var sortDescriptors: [NSSortDescriptor] {get}}
  1. Условие для выборки, если условие отсуствует - выборка применяется ко всему набору данных;

  2. Условие для сортировки - может как отсуствтвоать так и присутствовать.

Поскольку, в дальнейшем может возникнуть необходимость использовать отдельный NSPersistentContainer (например для тестирования), который, в свою очередь, выступает источником контекста - нужно закрыть его протоколом:

protocol DBContextProviding {   //1   func mainQueueContext() -> NSManagedObjectContext   //2   func performBackgroundTask(_ block: @escaping (NSManagedObjectContext) -> Void)}
  1. Контекст, с которым работает main Queue, необходим для использования NSFetchedResultsController;

  2. Требуется для выполнения операций с данными в фоновом потоке. Можно заменить на newBackgroundContext(). Про различия в работе этих двух методов можно прочитать тут.

Также, потребуются объекты, которые будут осуществлять конвертацию (мапинг) доменных моделей в объекты репозитория (NSManagedObject) и обратно:

class DBEntityMapper<DomainModel, Entity> {  //1   func convert(_ entity: Entity) -> DomainModel? {      fatalError("convert(_ entity: Entity: must be overrided")   }  //2   func update(_ entity: Entity, by model: DomainModel) {      fatalError("supdate(_ entity: Entity: must be overrided")   }  //3   func entityAccessorKey(_ entity: Entity) -> String {      fatalError("entityAccessorKey must be overrided")   }  //4   func entityAccessorKey(_ object: DomainModel) -> String {      fatalError("entityAccessorKey must be overrided")   }}
  1. Позволяет конвертировать NSManagedObject в доменную модель;

  2. Позволяет обновить NSManagedObject с помощью доменной модели;

  3. 4. - используется для связи между собой NSManagedObject и доменным объектом.

Когда-то, я использовал доменный объект, в качестве инициализатора NSManagedObject. С одной стороны, это было удобно, с другой стороны накладывало ряд ограничений. Например, когда использовались связи между объектами и один NSManagedObject создавал несколько других NSManagedObject. Такой подход размывал зоны ответственности и негативно сказывался на общей логике работы с данными.

Во время работы с репозиторием потребуется обрабатвать ошибки, для этого достаточно enum:

enum DBRepositoryErrors: Error {   case entityTypeError   case noChangesInRepository}

Здесь абстрактная часть репозитория подходит к концу, осталось самои интересное - реализация.

Реализация

Для данного примера, подойдет простая реализация DBContextProvider (без каких-либо дополнительных параметров):

final class DBContextProvider {   //1   private lazy var persistentContainer: NSPersistentContainer = {      let container = NSPersistentContainer(name: "DataStorageModel")           container.loadPersistentStores(completionHandler: { (_, error) in         if let error = error as NSError? {            fatalError("Unresolved error \(error),\(error.userInfo)")         }         container.viewContext.automaticallyMergesChangesFromParent = true      })      return container   }()   //2   private lazy var mainContext = persistentContainer.viewContext}//3//MARK:- DBContextProviding implementationextension DBContextProvider: DBContextProviding {   func performBackgroundTask(_ block: @escaping (NSManagedObjectContext) -> Void) {      persistentContainer.performBackgroundTask(block)   }   func mainQueueContext() -> NSManagedObjectContext {      self.mainContext   }}
  1. Инициализация NSPersistentContainer;

  2. Раньше такой подход избавлял от утечек памяти;

  3. Реализация DBContextProviding.

Основная составляющая репозитория выглядит следующим образом:

final class DBRepository<DomainModel, DBEntity>: Repository<DomainModel>, NSFetchedResultsControllerDelegate {      //1   private let associatedEntityName: String   //2   private let contextSource: DBContextProviding   //3   private var fetchedResultsController: NSFetchedResultsController<NSFetchRequestResult>?   //4   private var searchedData: Observable<[DomainModel]>?   //5   private let entityMapper: DBEntityMapper<DomainModel, DBEntity>      //6   init(contextSource: DBContextProviding, autoUpdateSearchRequest: RepositorySearchRequest?, entityMapper: DBEntityMapper<DomainModel, DBEntity>) {      self.contextSource = contextSource      self.associatedEntityName = String(describing: DBEntity.self)      self.entityMapper = entityMapper            super.init()     //7      guard let request = autoUpdateSearchRequest else { return }      self.searchedData  = .init(value: [])      //self.fetchedResultsController = configureactualSearchedDataUpdating(request)   }}
  1. Cвойство, которое будет использовать при работе с NSFetchRequest;

  2. DBContextProviding - для доступа к контексту, требуется для выполнения операций сохранения, загрузки, удаления;

  3. fetchedResultsController - необходим, когда требуется отслеживать изменения в NSPersistentStore (проще говоря, изменение объектов в базе);

  4. searchedData - зависит от fetchedResultsController и служит оберткой для fetchedResultsController, скрывая детали реализации и уведомляя подписчиков об изменениях в данных;

  5. entityMapper - конвертирует доменные объекты в NSManagedObject и обратно;

  6. Иницализатор;

  7. В случае если autoUpdateSearchReques != nil, выполняем конфигурацию fetchedResultsController для отслеживания изменениы в базе ;

Чтобы не порождать однотипный код для работы с контекстом, потребуется вспомогательный метод:

   private func applyChanges(context: NSManagedObjectContext, mergePolicy: Any = NSMergeByPropertyObjectTrumpMergePolicy, completion: ((Result<Void>) -> Void)? = nil) {      //1      context.mergePolicy = mergePolicy      switch context.hasChanges {      case true:         do {            //2            try context.save()         } catch {            ConsoleLog.logEvent(object: "DBRepository \(DBEntity.self)", method: "saveIn", "Error: \(error)")            completion?(Result.error(error))         }         ConsoleLog.logEvent(object: "DBRepository \(DBEntity.self)", method: "saveIn", "Saving Complete")         completion?(Result(value: ()))      case false:         //3         ConsoleLog.logEvent(object: "DBRepository \(DBEntity.self)", method: "saveIn", "No changes in context")         completion?(Result(error: DBRepositoryErrors.noChangesInRepository))      }   }
  1. mergePolicy - отвечает за то, как решаются конфликты при работе с контекстом. В данном случае, по умолчанию используется политика, которая отдает приоритет изменённым объектам, находящимся в памяти, а не в persistent store;

  2. Сохранение изменений в persistent store;

  3. Если изменения объектов отсутствуют, в completion-блок передается соответствующая ошибка.

Сохранение объектов реализовано следующим образом:

   private func saveIn(data: [DomainModel], clearBeforeSaving: RepositorySearchRequest?, completion: @escaping ((Result<Void>) -> Void))  {      contextSource.performBackgroundTask() { context in                  //1         if let clearBeforeSaving = clearBeforeSaving {            let clearFetchRequest = NSFetchRequest<NSManagedObject>(entityName: self.associatedEntityName)            clearFetchRequest.predicate = clearBeforeSaving.predicate            clearFetchRequest.includesPropertyValues = false            (try? context.fetch(clearFetchRequest))?.forEach({ context.delete($0) })         }                  //2         var existingObjects: [String: DBEntity] = [:]         let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: self.associatedEntityName)                  (try? context.fetch(fetchRequest) as? [DBEntity])?.forEach({            let accessor = self.entityMapper.entityAccessorKey($0)            existingObjects[accessor] = $0         })                  data.forEach({            let accessor = self.entityMapper.entityAccessorKey($0)            //3            let entityForUpdate: DBEntity? = existingObjects[accessor] ??  NSEntityDescription.insertNewObject(forEntityName: self.associatedEntityName, into: context) as? DBEntity            //4            guard let entity = entityForUpdate else { return }            self.entityMapper.update(entity, by: $0)         })         //5         self.applyChanges(context: context, completion: completion)      }   }
  1. Используется при необходимости удалить объекты, перед сохранением новых (в рамках текущего контекста);

  2. Выполняется выгрузка объектов, которые существуют в репозитории, для их дальнейшего изменения;

  3. Если объект с нужным entityAccessorKey отсутствует, создается новый экземпляр NSManagedObject;

  4. Выполнение мапинга свойств из доменного объекта в NSManagedObject;

  5. Применение выполненных изменений.

Важно: Возможно вас смутил п.2., данное решение оптимально на небольших наборах данных. Я выполнял замеры (ExampleCase3 в демо-проекте) на 10 000 записей, iPhone 6s Plus IOS 12.4.1 и получил следующие результаты:

  • время записи/перезаписи данных от 0,9 до 1.8 сек, cкачок потребления оперативной памяти в пике до 33 мб;

  • если убрать код в п2 и оставить только вставку новых объектов, то время записи/перезаписи данных +- 20 сек, cкачок потребления оперативной памяти в пике до 50 мб.

Для больших наборов данных я бы рекомендовал разделять их на части, использовать batchUpdate и batchDelete, а начиная с IOS 13 появился batchInsert.

Таким образом, реализация методов save cводится к вызову метода saveIn:

  override func save(_ objects: [DomainModel], completion: @escaping ((Result<Void>) -> Void)) {      saveIn(data: objects, clearBeforeSaving: nil, completion: completion)   }   override func save(_ objects: [DomainModel], clearBeforeSaving: RepositorySearchRequest, completion: @escaping ((Result<Void>) -> Void)) {      saveIn(data: objects, clearBeforeSaving: clearBeforeSaving, completion: completion)   }

Методы present, delete, eraseAllData завязаны на работе с NSFetchRequest. В их реализации нет ничего особенного, поэтому не вижу смысла заострять на них внимание:

 override func present(by request: RepositorySearchRequest, completion: @escaping ((Result<[DomainModel]>) -> Void)) {      //1      let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: associatedEntityName)      fetchRequest.predicate = request.predicate      fetchRequest.sortDescriptors = request.sortDescriptors      contextSource.performBackgroundTask() { context in         do {            //2            let rawData = try context.fetch(fetchRequest)            guard rawData.isEmpty == false else {return completion(Result(value: [])) }            guard let results = rawData as? [DBEntity] else {               completion(Result(value: []))               assertionFailure(DBRepositoryErrors.entityTypeError.localizedDescription)               return            }            //3            let converted = results.compactMap({ return self.entityMapper.convert($0) })            completion(Result(value: converted))         } catch {            completion(Result(error: error))         }      }   }      override func delete(by request: RepositorySearchRequest, completion: @escaping ((Result<Void>) -> Void)) {      //1      let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: associatedEntityName)      fetchRequest.predicate = request.predicate      fetchRequest.includesPropertyValues = false      contextSource.performBackgroundTask() { context in         //2         let results = try? context.fetch(fetchRequest)         results?.forEach({ context.delete($0) })         //3         self.applyChanges(context: context, completion: completion)      }   }   override func eraseAllData(completion: @escaping ((Result<Void>) -> Void)) {      //1      let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: associatedEntityName)      let batchDeleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)            batchDeleteRequest.resultType = .resultTypeObjectIDs      contextSource.performBackgroundTask({ context in         do {            //2            let result = try context.execute(batchDeleteRequest)            guard let deleteResult = result as? NSBatchDeleteResult,                  let ids = deleteResult.result as? [NSManagedObjectID]            else {               completion(Result.error(DBRepositoryErrors.noChangesInRepository))               return            }                        let changes = [NSDeletedObjectsKey: ids]            NSManagedObjectContext.mergeChanges(               fromRemoteContextSave: changes,               into: [self.contextSource.mainQueueContext()]            )            //3            completion(Result(value: ()))            return         } catch {            ConsoleLog.logEvent(object: "DBRepository \(DBEntity.self)", method: "eraseAllData", "Error: \(error)")            completion(Result.error(error))         }      })
  1. Создание запроса;

  2. Выборка объектов и их обработка;

  3. Вовзрат результата операции.

Для реализации возможности отслеживания изменений данных в реальном времени, потребуется FetchedResultsController. Для его конфигурации используется следующий метод:

 private func configureactualSearchedDataUpdating(_ request: RepositorySearchRequest) -> NSFetchedResultsController<NSFetchRequestResult> {      //1      let fetchRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: associatedEntityName)            fetchRequest.predicate = request.predicate      fetchRequest.sortDescriptors = request.sortDescriptors            //2      let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest,                                                                managedObjectContext: contextSource.mainQueueContext(), sectionNameKeyPath: nil,                                                                cacheName: nil)      //3      fetchedResultsController.delegate = self      try? fetchedResultsController.performFetch()      if let content = fetchedResultsController.fetchedObjects as? [DBEntity] {         updateObservableContent(content)      }      return fetchedResultsController   } func updateObservableContent(_ content: [DBEntity]) {      let converted = content.compactMap({ return self.entityMapper.convert($0) })   //4      searchedData?.value = converted   }
  1. Формирование запроса, на основании которого будут отслеживаться изменения;

  2. Создание экземпляра класса NSFetchedResultsController;

  3. performFetch() позволяет выполнить запрос и получить данные, не дожидаясь изменений в базе. Например, это может быть полезно при реализации Ofline First;

  4. Изменение свойства searchedData, в свою очередь уведомляет подписчиков (если такие имеются) об изменении.

Заключение

На этом этапе реализация всех основных методов для работы с репозиторием подходит к концу. Для меня основными преимуществами данного подхода стало следующее:

  • логика работы репозитория с Core Data стала везде единая;

  • для добавления новых объектов в репозиторий, достаточно создать только EntityMapper (новое Entity требуется создать в любом случае). Вся логика по мапингу свойств также собрана в одном месте;

  • Data слой стал более структурированным. Теперь можно точно гарантировать, что репозиторий не выполняет 100500 запросов в методе сохранения, чтобы проставить связи между объектами;

  • репозиторий легко можно подменить, например для тестов, или для отладки.

Подобный подход может подойти не всем, и это основной его минус. Часто логика работы с данными зависит в т.ч и от бэк-энда. Основная задача этого поста - донести один из концептов по работе с Core Data. Смотрите демо-проект, допиливайте его под себя, пользуйтесь!

Спасибо за внимание! Легкого кодинга, поменьше багов, побольше фич!

Подробнее..

Как расширить Spring своим типом Repository на примере Infinispan

27.12.2020 16:14:22 | Автор: admin

Зачем об этом писать?

Это моя первая статья, в ней я попытаюсь описать полученный мною практический опыт работы со Spring Repository под капотом фреймворка. Готовых статей про эту тему я в интернете не нашёл ни на русском, ни на английском, были только несколько репозиториев исходников на github, ну и исходники самого Spring. Поэтому и решил, почему бы не написать, вдруг тема написания своих типов репозиториев для Spring для кого-то ещё актуальна.

Программирование для Infinispan я не буду рассматривать подробно, детали реализации всегда можно посмотреть в исходниках, указанных в конце статьи. Основной упор сделан именно на сопряжение механизма Spring Boot Repository и нового типа репозитория.

С чего всё начиналось

В ходе работы на одном из проектов у одного из архитектора возникла идея, что можно написать свои типы репозиториев по аналогии, как это сделано в разных модулях Spring (например, JPARepository, KeyValueRepository, CassandraRepository и т.п.). В качестве пробной реализации решили выбрать работу с данными через Infinispan.

Естественно, что архитекторы - люди занятые, поэтому реализовывать идею поручили Java разработчику, т.е. мне.

Когда я начал прорабатывать тему в интернете, то Google упорно выдавал почти одни статьи про то, как замечательно использовать JPARepository во всех видах на тривиальных примерах. По KeyValueRepository информации было ещё меньше. На StackOverFlow печальные никем не отвеченные вопросы по подобной теме. Делать нечего, пришлось лезть в исходники Spring.

Infinispan

Если говорить кратко про Infinispan, то это всего лишь распределённое хранилище данных в виде ключ-значение, и всё это постоянно висит закэшированное в памяти. Перегружаем Infinispan, данные все обнуляются.

Было решено, что наиболее подходящий кандидат для исследования - KeyValueRepository, как самый близкий к данной области, уже реализованный в Spring. Вся разница только в том, что вместо Infinispan (на его месте мог быть и Hazelcast, например), как хранилища данных, в KeyValueRepository обычный ConcurrentHashMap.

Реализация

Чтобы в Spring проекте подключить возможность пользоваться репозиторием для хранилища ключ-значение пользуются аннотацией EnableMapRepositories.

@SpringBootApplication@EnableMapRepositories("my.person.package.for.entities")public class Application {    public static void main(String[] args) {        SpringApplication.run(Application.class, args);    }}

Можем практически полностью скопировать содержимое кода данной аннотации и создать свою EnableInfinispanRepositories.

Чтобы каждый раз это не писать, скажу, что слово map мы всегда заменяем на infinispan, в аналогичных реализациях, скрытых спойлерами.

Код аннотации EnableInfinispanRepositories
@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Inherited@Import(InfinispanRepositoriesRegistrar.class)public @interface EnableInfinispanRepositories {    String[] value() default {};    String[] basePackages() default {};    Class<?>[] basePackageClasses() default {};    ComponentScan.Filter[] excludeFilters() default {};    ComponentScan.Filter[] includeFilters() default {};    String repositoryImplementationPostfix() default "Impl";    String namedQueriesLocation() default "";    QueryLookupStrategy.Key queryLookupStrategy() default QueryLookupStrategy.Key.CREATE_IF_NOT_FOUND;    Class<?> repositoryFactoryBeanClass() default KeyValueRepositoryFactoryBean.class;    Class<?> repositoryBaseClass() default DefaultRepositoryBaseClass.class;    String keyValueTemplateRef() default "infinispanKeyValueTemplate";    boolean considerNestedRepositories() default false;}

Если посмотреть что происходит в коде аннотации EnableMapRepositories, то увидим, что там импортируется класс, который и делает всю магию по активации данного типа репозитория.

@Import(MapRepositoriesRegistrar.class)public @interface EnableMapRepositories {}

Ниже код MapRepositoriesRegistar.

public class MapRepositoriesRegistrar extends RepositoryBeanDefinitionRegistrarSupport {@Overrideprotected Class<? extends Annotation> getAnnotation() {return EnableMapRepositories.class;}@Overrideprotected RepositoryConfigurationExtension getExtension() {return new MapRepositoryConfigurationExtension();}}

В коде перегружаются два метода. В одном связывается Registar со своей активирующей аннотацией, чтобы потом из неё получить заполненные атрибуты конфигурации. В другом находится реализация хранилища данных, специфичных для данного типа репозитория.

Сделаем по аналогии свой InfinispaRepositoriesRegistar.
@NoArgsConstructorpublic class InfinispanRepositoriesRegistrar extends RepositoryBeanDefinitionRegistrarSupport {    @Override    protected Class<? extends Annotation> getAnnotation() {        return EnableInfinispanRepositories.class;    }    @Override    protected RepositoryConfigurationExtension getExtension() {        return new InfinispanRepositoryConfigurationExtension();    }}

Теперь посмотрим, как же выглядит сама реализация хранилища.

public class MapRepositoryConfigurationExtension extends KeyValueRepositoryConfigurationExtension {@Overridepublic String getModuleName() {return "Map";}@Overrideprotected String getModulePrefix() {return "map";}@Overrideprotected String getDefaultKeyValueTemplateRef() {return "mapKeyValueTemplate";}@Overrideprotected AbstractBeanDefinition getDefaultKeyValueTemplateBeanDefinition(RepositoryConfigurationSource configurationSource) {BeanDefinitionBuilder adapterBuilder = BeanDefinitionBuilder.rootBeanDefinition(MapKeyValueAdapter.class);adapterBuilder.addConstructorArgValue(getMapTypeToUse(configurationSource));BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(KeyValueTemplate.class);    ...  }  ...}

В MapKeyValueAdapter будет реализована самая специфическая часть, характерная именно для локального хранения кэша в HashMap. А вот KeyValueTemplate оборачивает методы адаптера довольно общим кодом.

Поэтому чтобы выполнить задачу и заменить хранение данных с локального кэша на распределённое хранилище Infinispan, нужно сделать аналогичный ConfigurationExtension, но заменить на специфичный адаптер, где и будет реализована вся логика чтения/записи/поиска данных, характерная для Infinispan.

Реализация InfinispanRepositoriesConfigurationExtension
@NoArgsConstructorpublic class InfinispanRepositoryConfigurationExtension extends KeyValueRepositoryConfigurationExtension {    @Override    public String getModuleName() {        return "Infinispan";    }    @Override    protected String getModulePrefix() {        return "infinispan";    }    @Override    protected String getDefaultKeyValueTemplateRef() {        return "infinispanKeyValueTemplate";    }    @Override    protected Collection<Class<?>> getIdentifyingTypes() {        return Collections.singleton(InfinispanRepository.class);    }    @Override    protected AbstractBeanDefinition getDefaultKeyValueTemplateBeanDefinition(RepositoryConfigurationSource configurationSource) {        RootBeanDefinition infinispanKeyValueAdapterDefinition = new RootBeanDefinition(InfinispanKeyValueAdapter.class);        RootBeanDefinition keyValueTemplateDefinition = new RootBeanDefinition(KeyValueTemplate.class);        ConstructorArgumentValues constructorArgumentValuesForKeyValueTemplate = new ConstructorArgumentValues();        constructorArgumentValuesForKeyValueTemplate.addGenericArgumentValue(infinispanKeyValueAdapterDefinition);        keyValueTemplateDefinition.setConstructorArgumentValues(constructorArgumentValuesForKeyValueTemplate);        return keyValueTemplateDefinition;    }}

Нужно обязательно в нашем ConfigurationExtension ещё перегрузить метод getIdentifyingTypes(), чтобы в нём сослаться на наш новый тип репозитория (см. реализацию выше).

@NoRepositoryBeanpublic interface InfinispanRepository <T, ID> extends PagingAndSortingRepository<T, ID> {}

Чтобы окончательно всё заработало, нужно сконфигурировать KeyValueTemplate, подсунув ему наш адаптер.

@Configurationpublic class InfinispanConfiguration extends CachingConfigurerSupport {    @Autowired    private ApplicationContext applicationContext;    @Bean    public InfinispanKeyValueAdapter getInfinispanAdapter() {        return new InfinispanKeyValueAdapter(                applicationContext.getBean(CacheManager.class)        );    }    @Bean("infinispanKeyValueTemplate")    public KeyValueTemplate getInfinispanKeyValueTemplate() {        return new KeyValueTemplate(getInfinispanAdapter());    }}

На этом всё.

Можно, конечно, копать глубже и не пользоваться готовыми Spring-овыми реализациями для репозиториев, а наследоваться исключительно от их абстрактных классов и интерфейсов, но объём работ будет намного больше, чем в этой статье.

Резюме

Написав всего 6 своих классов, мы получили новый тип репозитория, который может работать с Infinispan в качестве хранилища данных. И работает этот новый тип репозитория очень похоже на стандартные Spring репозитории.

Полный комплект исходников можно найти на моём github.

Исходники Spring Data KeyValue можно увидеть также на github.

Если у вас есть конструктивные замечания к данной реализации, то пишите в комментариях, либо можете сделать pull request в исходном проекте.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru