Как да използвате базата данни Xodus в приложения на Kotlin

Снимка на Ян Антонин Колар на Unsplash

Искам да ви покажа как да използвам един от любимите ми избори за база данни за приложения на Kotlin. А именно Xodus. Защо обичам да използвам Xodus за приложения на Kotlin? Е, ето няколко от неговите продажби:

  • Транзакционното
  • Embedded
  • Schema-малко
  • Чиста основа на JVM
  • Има допълнителен Kotlin DSL - Xodus-DNQ.

Какво означава това за вас?

  • ACID на борда - всички операции с база данни са атомни, последователни, изолирани и издръжливи.
  • Няма нужда да управлявате външна база данни - всичко е вътре в приложението ви.
  • Безболезнени рефактори - ако трябва да добавите няколко свойства, няма да се налага да възстановявате таблиците.
  • Кросплатформена база данни - Xodus може да работи на всяка платформа, която може да работи с виртуална машина на Java.
  • Ползи от езика на Kotlin - вземете най-доброто от използването на типове, нулируеми стойности и делегати за деклариране на свойства и описание на ограничения.

Xodus е продукт с отворен код от JetBrains. Първоначално той е разработен за вътрешна употреба, но впоследствие е пуснат на публиката още през юли 2016 г. YouTrack issue tracker и Hub екипният инструмент го използват като съхранение на данни. Ако се интересувате от представянето, можете да разгледате показателите. Що се отнася до примера от реалния живот, разгледайте инсталацията на JetBrains YouTrack: която към момента на писане има над 1,6 милиона броя и това дори не отчита всички коментари и записи за проследяване на времето, съхранявани там.

Xodus-DNQ е библиотека на Kotlin, която съдържа езика за дефиниране на данни и заявки за Xodus. Той също така е разработен първо като част от продукта и след това по-късно пуснат публично. И YouTrack и Hub го използват за дефиниране на устойчиви слоеве.

Настройвам

Да напишем малко приложение, което съхранява книги и техните автори.

Ще използвам Gradle като инструмент за изграждане, тъй като помага да се опростят всички неща за управление на зависимости и съставяне на проекти. Ако никога не сте работили с Gradle, препоръчвам да разгледате официалните ръководства, които имат за инсталирането и създаването на нови компилации.

Така че първо, трябва да започнем със създаването на нова директория за нашия пример и след това да стартираме gradle init там. Това ще инициализира структурата на проекта и ще добави някои директории и изграждане на скриптове.

Сега създайте файл bookstore.kt в директорията src / main / kotlin. Напълнете го с никога не излизащата от мода класика:

забавно главно () {
  println ("Здравей свят")
}

След това актуализирайте файла build.gradle, като използвате код, подобен на този:

плъгини {
  id 'приложение'
  id 'org.jetbrains.kotlin.jvm' версия '1.3.21'
}
група 'mariyadavydova'
версия '1.0-SNAPSHOT'
sourceCompatibility = 1.8
targetCompatibility = 1.8
task.withType (org.jetbrains.kotlin.gradle.tasks.KotlinCompile) .all {
  kotlinOptions {
    jvmTarget = "1.8"
  }
}
хранилища {
  mavenCentral ()
}
зависимости {
  внедряване 'org.jetbrains.kotlin: kotlin-stdlib-jdk8: 1.3.21'
  внедряване „org.jetbrains.xodus: dnq: 1.2.420“
}
mainClassName = 'BookstoreKt'

Има няколко неща, които се случват тук:

  1. Добавяме плъгин Kotlin и твърдим, че изходът на компилацията е насочен към JVM 1.8.
  2. Добавяме зависимости към стандартната библиотека на Kotlin и Xodus-DNQ.
  3. Също така добавяме приставката за приложение и определяме основния клас. В случая на приложението Kotlin, ние нямаме клас с основен статичен метод, като в Java. Вместо това трябва да определим самостоятелна функция main. Въпреки това, под капака, Kotlin все още прави клас, съдържащ тази функция, а името на класа се генерира от името на файла. Например „bookstore.kt“ прави „BookstoreKt“.

Всъщност можем безопасно да премахнем settings.gradle, тъй като не ни трябват в този пример.

Сега, изпълнете ./gradlew run; трябва да видите "Hello World" в конзолата си:

> Задача: пусни
Здравей свят

Дефиниция на данни

Снимка на Алфонс Моралес на Unsplash

Xodus предоставя три различни начина за справяне с данни, а именно среда, магазини за субекти и виртуалната файлова система. Въпреки това Xodus-DNQ поддържа само Entity Stores, които описват модел на данни като набор от въведени свойства с именани свойства (атрибути) и имена на връзки (отношения). Подобно е на редовете в таблицата на базата данни на SQL.

Тъй като целта ми е да демонстрирам колко е лесно да управлявам Xodus чрез Kotlin DSL, ще се придържам към API на типове субекти за тази история.

Да започнем с XdAuthor:

клас XdAuthor (образувание: субект): XdEntity (образувание) {
  придружаващ обект: XdNaturalEntityType  ()
var име от xdRequiredStringProp ()
  var countryOfBirth от xdStringProp ()
  var yearOfBirth от xdRequiredIntProp ()
  var yearOfDeath от xdNullableIntProp ()
  вал книги от xdLink0_N (XdBook :: автори)
}

От моя гледна точка тази декларация изглежда доста естествено: казваме, че нашите автори винаги имат имена и година на раждане, може да имат страна на раждане и година на смъртта (последната е без значение за живеещите в момента автори); също така може да има произволен брой книги от всеки автор в нашата книжарница.

Има няколко неща, които си струва да се споменат в този кодов фрагмент:

  • Обектът придружител декларира свойството entitType за всеки клас (който се използва от двигателя на базата данни).
  • Полетата за данни се декларират с помощта на делегатите, които капсулират типовете, свойствата и ограниченията за тези полета.
  • Връзките са стойности, а не променливи; това означава, че не ги задавате с =, а получавате достъп до тях като колекция. (Обърнете внимание на вал книги срещу името var; прекарах доста време в опити да разбера защо компилацията с var книги продължава да се проваля.)

Вторият тип е XdBook:

клас XdBook (субект: Субект): XdEntity (образувание) {
  придружаващ обект: XdNaturalEntityType  ()
var заглавие от xdRequiredStringProp ()
  var година от xdNullableIntProp ()
  val жанрове от xdLink1_N (XdGenre)
  val автори: XdMutableQuery  от xdLink1_N (XdAuthor :: книги)
}

Това, на което трябва да обърнете внимание, е декларацията на полето на авторите:

  • Забележете, че записваме типа изрично (XdMutableQuery ). За двупосочната връзка трябва да помогнем на компилатора да разреши типовете, като оставим намек за един от краищата на връзката.
  • Също така забележете, че XdAuthor :: книги препраща към XdBook :: автори и обратно. Трябва да добавим тези препратки, ако искаме връзката да е двупосочна; така че ако добавите автор към книгата, тя ще се появи в списъка на книгите на този автор и обратно.

Третият тип субект е изброяване на XdGenre, което е доста тривиално:

клас XdGenre (образувание: субект): XdEnumEntity (образувание) {
 придружаващ обект: XdEnumEntityType  () {
   val FANTASY от enumField {}
   val ROMANCE от enumField {}
 }
}

Инициализация на база данни

Сега, когато декларирахме типовете субекти, трябва да инициализираме базата данни:

забавно initXodus (): TransientEntityStore {
  XdModel.registerNodes (
      XdAuthor,
      XdBook,
      XdGenre
  )
  val databaseHome = Файл (System.getProperty ("user.home"), "bookstore")
  val store = StaticStoreContainer.init (
      dbFolder = databaseHome,
      environmentName = "db"
  )
  initMetaData (XdModel.hierarchy, магазин)
  връщане магазин
}
забавно главно () {
  val магазин = initXodus ()
}

Този код показва най-основната настройка:

  • Дефинираме модела на данните. Тук изброяваме всички типове субекти ръчно, но е възможно и автоматично сканиране на classpath.
  • Инициализираме магазина на базата данни в папка {user.home} / bookstore.
  • Свързваме метаданните с магазина.

Попълване на данните в

Снимка на Ани Спрат на Unsplash

Сега, когато инициализирахме базата данни, е време да поставим нещо вътре. Преди да направим това, нека добавим toString методи към нашите учебни класове. Единствената им цел е да ни позволят да извеждаме съдържанието на базата данни в четим от човека формат.

клас XdAuthor (образувание: субект): XdEntity (образувание) {
  ...
  отменя забавление toString (): String {
    val bibliography = books.asSequence (). joinToString ("\ n")
    върнете "$ name ($ yearOfBirth - $ {yearOfDeath?:" ??? "}): \ n $ библиография"
  }
}
клас XdBook (субект: Субект): XdEntity (образувание) {
  ...
  отменя забавление toString (): String {
    val жанрове = genres.asSequence (). joinToString (",")
    връщане "$ title ($ {year ?:" Unknown "}) - $ жанрове"
  }
}
клас XdGenre (образувание: субект): XdEnumEntity (образувание) {
  ...
  отменя забавление toString (): String {
    върнете this.name.toLowerCase (). capitalize ()
  }
}

Забележете books.asSequence (). JoinToString ("\ n") и genres.asSequence (). Инструкции joinToString (","): тук използваме метода asSequence () за преобразуване на XdQuery в колекция на Kotlin.

Точно, нека сега добавим няколко книги от нашата колекция вътре в основната функция. Всички операции с база данни (създаване, четене, актуализиране и премахване на субекти), които извършваме вътре в транзакции - модификации на атомната база данни, което гарантира запазване на последователността.

В случая с нашата книжарница има много начини да я напълните с неща:

1. Добавете автор и книга отделно:

 val bronte = store.transactional {
   XdAuthor.new {
     name = "Шарлът Бронте"
     countryOfBirth = "Англия"
     годинаOfBirth = 1816
     годинаOfDeath = 1855
   }
 }
 store.transactional {
   XdBook.new {
     title = "Джейн Айър"
     година = 1847
     genres.add (XdGenre.ROMANCE)
     authors.add (Bronte)
   }
 }

2. Добавете автор и поставете няколко книги в списъка си:

 val tolkien = store.transactional {
   XdAuthor.new {
     име = "J. R. R. Tolkien"
     countryOfBirth = "Англия"
     годинаOfBirth = 1892
     годинаOfDeath = 1973
   }
 }
 store.transactional {
   tolkien.books.add (XdBook.new {
     title = "Хобитът"
     година = 1937
     genres.add (XdGenre.FANTASY)
   })
   tolkien.books.add (XdBook.new {
     title = "Властелинът на пръстените"
     година = 1955
     genres.add (XdGenre.FANTASY)
   })
 }

3. Добавете автор с книги:

 store.transactional {
   XdAuthor.new {
     name = "Джордж Р. Р. Мартин"
     countryOfBirth = "САЩ"
     годинаOfBirth = 1948
     books.add (XdBook.new {
       title = "Игра на тронове"
       година = 1996
       genres.add (XdGenre.FANTASY)
     })
   }
 }

За да проверим дали всичко е създадено, всичко, което трябва да направим, е да отпечатаме съдържанието на нашата база данни:

 store.transactional (само за четене = вярно) {
   println (XdAuthor.all (). asSequence (). joinToString ( "\ н *** \ Н"))
 }

Сега, ако изпълните ./gradlew run, трябва да видите следния изход:

Шарлът Бронте (1816-1855):
Jane Eyre (1847) - Романтика
***
J. R. R. Tolkien (1892-1973):
Хобитът (1937) - фентъзи
Властелинът на пръстените (1955 г.) - фентъзи
***
Джордж Р. Р. Мартин (1948 - ???):
Игра на тронове (1996) - фентъзи

Ограничения

Както бе споменато, транзакциите гарантират последователност на данните. Една от операциите, които Xodus прави преди да запази промените, е проверка на ограниченията. В DNQ някои от тях са кодирани в името на делегата, който предоставя свойство на даден тип. Например, xdRequiredIntProp винаги трябва да бъде зададен на някаква стойност, докато xdNullableIntProp може да остане празен.

Въпреки това Xodus-DNQ позволява да се определят по-сложни ограничения, които са описани в официалната документация. Добавих няколко примера към типа организация на XdAuthor:

  var име от xdRequiredStringProp {съдържаNone ("?!")}
  вар страна от xdStringProp {
    дължина (мин. = 3, макс. = 56)
    регулярен (Regex ( "[A-Za-Z.,] +"))
  }
  var yearOfBirth от xdRequiredIntProp {max (2019)}
  var yearOfDeath от xdNullableIntProp {max (2019)}

Може би се чудите защо съм ограничил дължината на свойството countryOfBirth до 56 знака. Е, най-дългото официално име на държавата, което открих, е „Обединеното кралство Великобритания и Северна Ирландия“ - точно 56 знака!

Заявки

Вече използвахме заявки към база данни. Помниш ли? Ние отпечатахме списъка с автори, използвайки XdAuthor.all (). AsSequence (). Както може би се досещате, методът all () връща всички записи от даден тип субект.

По-често, отколкото не, ще предпочитаме филтрирането на данни. Ето няколко примера:

store.transactional (само за четене = вярно) {
  val fantasyBooks = XdBook.filter {
    it.genres съдържа XdGenre.FANTASY}
  val booksOf20thCentury = XdBook.filter {
    (it.year ge 1900) и (it.year lt 1999)}
  val авториFromEngland = XdAuthor.filter {
    it.countryOfBirth eq "Англия"}
  
  val книгиSortedByYear = XdBook.all (). sortedBy (XdBook :: година)
  val allGenres = XdBook.all (). flatMapDistinct (XdBook :: жанрове)
}

Отново има много опции за изграждане на заявки за данни, затова горещо препоръчвам да разгледате документацията.

Надявам се тази история да е толкова полезна за вас, колкото и за мен, когато я написах :) Всяка обратна връзка е високо оценена!

Можете да намерите изходния код за този урок тук.