Введение
Немного о сабже. BerkleyDB — высокопроизводительная встраиваемая СУБД, поставляемая в виде библиотеки для различных языков программирования. Это решение предполагает хранение пар ключ-значение, также поддерживается возможность ставить одному ключу в соответствие несколько значений. BerkleyDB поддерживает работу в многопоточной среде, репликацию, и многое другое. Внимание данной статьи будет обращено в первую очередь в сторону использования библиотеки, предоставленной Sleepycat Software в бородатых 90х. В этой статье будут рассмотрены основные аспекты работы с DPL (Direct Persistence Layer) API.
Примечание: все примеры в данной статье будут приведены на языке Kotlin.
Описание сущностей
Для начала, ознакомимся со способом описания сущностей. К счастью, он весьма схож на JPA. Все сущности отражаются в виде классов с аннотациями
@Persistent
и @Entity
, каждый из которых позволяет указать в явном виде версию описываемой сущности. В рамках этой статьи, мы будем пользоваться только аннотацией @Entity
, в последующих — будет пролит свет и на @Persitent
@Entity(version = SampleDBO.schema)
class SampleDBO private constructor() {
companion object {
const val schema = 1
}
@PrimaryKey
lateinit var id: String
private set
@SecondaryKey(relate = Relationship.MANY_TO_ONE)
lateinit var name: String
private set
constructor(id: String, name: String): this() {
this.id = id
this.name = name
}
}
Примечание: для ключа с аннотацией
@PrimaryKey
типа java.lang.Long
можно также указать параметр sequence, который создаст отдельную последовательность для генерации идентификаторов ваших сущностей. Увы, в Котлине не работает.Отметить стоит отдельно, что: во-первых, во всех сущностях требуется оставить приватный конструктор по-умолчанию для корректной работы библиотеки, во-вторых — аннотация
@SecondaryKey
должна присутствовать в каждом поле сущности, по которому мы в дальнейшем хотим осуществлять индексирование. В данном случае, это поле name.Использование constraints
Для использования constraint-ов в сущностях, создатели предлагают вполне прямолинейный способ — осуществлять верификацию внутри аксессоров. Модифицируем пример выше для наглядности.
@Entity(version = SampleDBO.schema)
class SampleDBO private constructor() {
companion object {
const val schema = 1
}
@PrimaryKey
lateinit var id: String
private set
@SecondaryKey(relate = Relationship.MANY_TO_ONE)
var name: String? = null
private set(value) {
if(value == null) {
throw IllegalArgumentException("Illegal name passed: ${value}. Non-null constraint failed")
}
if(value.length < 4 || value.length > 16) {
throw IllegalArgumentException("Illegal name passed: ${value}. Expected length in 4..16, but was: ${value.length}")
}
}
constructor(id: String, name: String): this() {
this.id = id
this.name = name
}
}
Отношения между сущностями
BerkleyDB JE поддерживает все типы отношений:
- 1:1
Relationship.ONE_TO_ONE
- 1:N
Relationship.ONE_TO_MANY
- N:1
Relationship.MANY_TO_ONE
- N:M
Relationship.MANY_TO_MANY
Для описания отношения между сущностями используется все тот же
@SecondaryKey
с тремя дополнительными параметрами:relatedEntity
— класс сущности, отношение к которой описываетсяonRelatedEntityDelete
— поведение, при удалении сущности (прерывание транзакции, обнуление ссылок, каскадное удаление)name
— поле сущности, которое выступает в роли foreign key
@Entity(version = CustomerDBO.schema)
class CustomerDBO private constructor() {
companion object {
const val schema = 1
}
@PrimaryKey()
var id: String? = null
private set
@SecondaryKey(relate = Relationship.ONE_TO_ONE)
lateinit var email: String
private set
var balance: Long = 0L
constructor(email: String, balance: Long): this() {
this.email = email
this.balance = balance
}
constructor(id: String, email: String, balance: Long): this(email, balance) {
this.id = id
}
override fun toString(): String {
return "CustomerDBO(id=$id, email=$email, balance=$balance)"
}
}
@Entity(version = ProductDBO.schema)
class ProductDBO {
companion object {
const val schema = 1
}
@PrimaryKey()
var id: String? = null
private set
@SecondaryKey(relate = Relationship.MANY_TO_ONE)
lateinit var name: String
private set
var price: Long = 0L
var amount: Long = 0L
private constructor(): super()
constructor(name: String, price: Long, amount: Long): this() {
this.name = name
this.price = price
this.amount = amount
}
constructor(id: String, name: String, price: Long, amount: Long): this(name, price, amount) {
this.id = id
}
override fun toString(): String {
return "ProductDBO(id=$id, name=$name, price=$price, amount=$amount)"
}
}
@Entity(version = ProductChunkDBO.schema)
class ProductChunkDBO {
companion object {
const val schema = 1
}
@PrimaryKey()
var id: String? = null
private set
@SecondaryKey(relate = Relationship.MANY_TO_ONE, relatedEntity = OrderDBO::class, onRelatedEntityDelete = DeleteAction.CASCADE)
var orderId: String? = null
private set
@SecondaryKey(relate = Relationship.MANY_TO_ONE, relatedEntity = ProductDBO::class, onRelatedEntityDelete = DeleteAction.CASCADE)
var itemId: String? = null
private set
var amount: Long = 0L
private constructor()
constructor(orderId: String, itemId: String, amount: Long): this() {
this.orderId = orderId
this.itemId = itemId
this.amount = amount
}
constructor(id: String, orderId: String, itemId: String, amount: Long): this(orderId, itemId, amount) {
this.id = id
}
override fun toString(): String {
return "ProductChunkDBO(id=$id, orderId=$orderId, itemId=$itemId, amount=$amount)"
}
}
@Entity(version = OrderDBO.schema)
class OrderDBO {
companion object {
const val schema = 1
}
@PrimaryKey()
var id: String? = null
private set
@SecondaryKey(relate = Relationship.MANY_TO_ONE, relatedEntity = CustomerDBO::class, onRelatedEntityDelete = DeleteAction.CASCADE)
var customerId: String? = null
private set
@SecondaryKey(relate = Relationship.ONE_TO_MANY, relatedEntity = ProductChunkDBO::class, onRelatedEntityDelete = DeleteAction.NULLIFY)
var itemChunkIds: MutableSet<String> = HashSet()
private set
var isExecuted: Boolean = false
private set
private constructor()
constructor(customerId: String, itemChunkIds: List<String> = emptyList()): this() {
this.customerId = customerId
this.itemChunkIds.addAll(itemChunkIds)
}
constructor(id: String, customerId: String, itemChunkIds: List<String> = emptyList()): this(customerId, itemChunkIds) {
this.id = id
}
fun setExecuted() {
this.isExecuted = true
}
override fun toString(): String {
return "OrderDBO(id=$id, customerId=$customerId, itemChunkIds=$itemChunkIds, isExecuted=$isExecuted)"
}
}
Конфигурация
BerkleyDB JE предоставляет широкие возможности для конфигурации. В данной статье будут покрыты минимально необходимые для написания клиентского приложения настройки, в дальнейшем, по мере возможности, свет будет пролит и на более продвинутые возможности.
Для начала, рассмотрим точки входа в компонент, который будет работать с базой данных. В нашем случае это будут классы
Environment
и EntityStore
. Каждый из них предоставляет внушительный перечень различных опций. Environment
Настройка работы с окружением предполагает определение стандартных параметров. В самом простом варианте выйдет что-то подобное:
val environment by lazy {
Environment(dir, EnvironmentConfig().apply {
transactional = true
allowCreate = true
nodeName = "SampleNode_1"
cacheSize = Runtime.getRuntime().maxMemory() / 8
offHeapCacheSize = dir.freeSpace / 8
})
}
transactional
— устанавливаем какtrue
, если хотим использовать транзакцииallowCreate
— устанавливаем какtrue
, если окружение должно быть создано, если его не будет обнаружено в указанной директорииnodeName
— устанавливаем название для конфигурируемого Environment; очень приятная опция, в случае, если в приложении будет использоваться несколько Environment, и хочетсяне прострелить себе ногуиметь читаемые логиcacheSize
— количество памяти, которое будет отводиться под in-memory кэшoffHeapCacheSize
— количество памяти, которое будет отводиться под дисковый кэш
EntityStore
В случае, если в приложении используется DPL API, основным классом для работы с базой данных будет EntityStore. Стандартная конфигурация выглядит следующим образом:
val store by lazy {
EntityStore(environment, name, StoreConfig().apply {
transactional = true
allowCreate = true
})
}
Индексы, доступ к данным
Для того, чтобы понять, как работают индексы, проще всего рассмотреть такой SQL-запрос:
SELECT * FROM customers ORDER BY email;
В BerkleyDB JE этот запрос можно осуществить следующим образом: первое, что потребуется, это, собственно, создать два индекса. Первый — основной, он должен соответствовать
@PrimaryKey
нашей сущности. Второй — вторичный, соответствующий полю, упорядочивание по которому производится (примечание — поле должно, как выше было сказано, быть аннотировано как @SecondaryKey
). val primaryIndex: PrimaryIndex<String, CustomerDBO> by lazy {
entityStore.getPrimaryIndex(String::class.java, CustomerDBO::class.java)
}
val emailIndex: SecondaryIndex<String, String, CustomerDBO> by lazy {
entityStore.getSecondaryIndex(primaryIndex, String::class.java, "email")
}
Получение выборки данных осуществляется привычным способом — используя интерфейс курсора (в нашем случае —
EntityCursor
) fun read(): List<CustomerDBO> = emailIndex.entities().use { cursor ->
mutableListOf<CustomerDBO>().apply {
var currentPosition = 0
val count = cursor.count()
add(cursor.first() ?: return@apply)
currentPosition++
while(currentPosition < count) {
add(cursor.next() ?: return@apply)
currentPosition++
}
}
}
Relations & Conditions
Частой задачей является получение сущностей, используя связь между их таблицами. Рассмотрим этот вопрос на примере следующего SQL запроса:
SELECT * FROM orders WHERE customer_id = ?;
И его представление в рамках Berkley:
fun readByCustomerId(customerId: String): List<OrderDBO> =
customerIdIndex.subIndex(customerId).entities().use { cursor ->
mutableListOf<OrderDBO>().apply {
var currentPosition = 0
val count = cursor.count()
add(cursor.first() ?: return@apply)
currentPosition++
while(currentPosition < count) {
add(cursor.next() ?: return@apply)
currentPosition++
}
}
}
К сожалению, данный вариант возможен только при одном условии. Для создания запроса с несколькими условиями потребуется использовать более сложную конструкцию.
@Entity(version = CustomerDBO.schema)
class CustomerDBO private constructor() {
companion object {
const val schema = 1
}
@PrimaryKey()
var id: String? = null
private set
@SecondaryKey(relate = Relationship.ONE_TO_ONE)
lateinit var email: String
private set
@SecondaryKey(relate = Relationship.MANY_TO_ONE)
lateinit var country: String
private set
@SecondaryKey(relate = Relationship.MANY_TO_ONE)
lateinit var city: String
private set
var balance: Long = 0L
constructor(email: String, country: String, city: String, balance: Long): this() {
this.email = email
this.country = country
this.city = city
this.balance = balance
}
constructor(id: String, email: String, country: String, city: String, balance: Long): this(email, country, city, balance) {
this.id = id
}
}
val countryIndex: SecondaryIndex<String, String, CustomerDBO> by lazy {
entityStore.getSecondaryIndex(primaryIndex, String::class.java, "country")
}
val cityIndex: SecondaryIndex<String, String, CustomerDBO> by lazy {
entityStore.getSecondaryIndex(primaryIndex, String::class.java, "city")
}
SELECT * FROM customers WHERE country = ? AND city = ?;
fun readByCountryAndCity(country: String, city: String): List<CustomerDBO> {
val join = EntityJoin<String, CustomerDBO>(primaryIndex)
join.addCondition(countryIndex, country)
join.addCondition(cityIndex, city)
return join.entities().use { cursor ->
mutableListOf<CustomerDBO>().apply {
var currentPosition = 0
val count = cursor.count()
add(cursor.first() ?: return@apply)
currentPosition++
while(currentPosition < count) {
add(cursor.next() ?: return@apply)
currentPosition++
}
}
}
}
Как видно из примеров — довольно муторный синтаксис, но жить вполне можно.
Range queries
С данным типом запросов все прозрачно, у индексов есть перегрузка функции
fun <E> entities(fromKey: K, fromInclusive: Boolean, toKey: K, toInclusive: Boolean):
EntityCursor<E>
которая предоставляет возможность использовать курсор, итерирующийся по нужной выборке данных. Этот метод вполне быстро работает, так как используются индексы, сравнительно удобен, и, на мой взгляд, не требует отдельных комментариев.Вместо заключения
Это первая статья из планируемого цикла по BerkleyDB. Основаня ее цель — познакомить читателя с основами работы с Java Edition библиотекой, рассмотреть основные возможности, которые необходимы для рутинных действий. В последующих статьях будут покрыты более интересные детали работы с этой библиотекой, если статья окажется кому-то интересной.
Поскольку опыта работы с Berkley у меня совсем немного — буду признателен за критику и поправки в комментариях, если я где-то допустил огрехи.
Комментарии (10)
inf0man
23.08.2017 00:23Мы тоже в проекте использовали BerkleyDB, но потом переехали на WiredTiger. WoredTiger более современный, производительный. Кстати, его писали чуть ли не те же люди, что и сам BerkleyDB.
Beholder
23.08.2017 21:05Смотрю в Википедии, у них произошли изменения в лицензировании:
Oracle Berkeley DB JE 7.3.7 is licensed under the Apache License, Version 2.0
Означает ли это, что JE теперь можно использовать в продуктах с закрытыми исходниками?KomarovI Автор
23.08.2017 22:01Не совсем так, в википедии указано:
В рамках техники двойного лицензирования Oracle также распространяет проприетарную лицензию на использование библиотеки в закрытых проектах.
Поэтому просто взять и использовать не получится. Требуется получиться соответствующую лицензию отвеликой и могучей корпорации злаOracleBeholder
24.08.2017 00:08Русская версия статьи сильно устаревшая, там значится последняя версия 6.0.20 (от 2013 года), и упомянута лицензия только AGPLv3, но Oracle её изменили (см. предыдущий коммент). Вопрос остаётся открытым. Насколько знаю, лицензия Apache не «вирусная» и допускает использование компонентов под ней в коммерческих закрытых продуктах.
KomarovI Автор
24.08.2017 12:23На сайте Oracle (http://www.oracle.com/technetwork/database/berkeleydb/downloads/licensing-098979.html) указано:
If you do not want to release the source code for your application, you may purchase a license from Oracle. For pricing information, or if you have further questions on licensing, please contact us at berkeleydb-info_us@oracle.com.
Так что нет, просто так взять и использовать в закрытом проекте-таки нельзя.Beholder
24.08.2017 12:42Я боюсь, что и эта страница устарела. Там ни слова нет про Apache License, ни в тексте, ни по ссылкам.
KomarovI Автор
24.08.2017 18:30В таком случае советую попробовать написать на почту выше с вашим вопросом. Думаю, это лучшее решение
Qwertovsky
24.08.2017 19:31На форуме уже спрашивали про устаревшие ссылки: https://community.oracle.com/thread/4030451. Ответ: не везде обновили.
И в помнике лицензию добавили http://download.oracle.com/maven/com/sleepycat/je/7.4.5/je-7.4.5.pom
В низу страницы http://www.oracle.com/technetwork/database/database-technologies/berkeleydb/learnmore/index.html лицензия новая.
На всё скаченное она должна распространяться.
andreylartsev
Лет 15 назад мне потребовалась легковесная встраиваемая БД и я попробовал Berkeley DB. Программка тогда составлялась на CBuilder.
Уже тогда оно было анахронизмом но ещё не было куплено Oracle. Мне понравилось что в ней были транзакции, а БД натурально хранилась в нескольких файликах датафайл + редулоги, а API ужасно не понравилось ) Но в чем суть вопроса…
Вот прошло 15 лет и ничего не изменилось ) И меня до сих пор мучают два вопроса: 1) зачем Oracle это купил когда то, 2) И зачем это вообще люди до сих пор используют для новых систем.
Неужели есть какие то преимущества по сравнению с SQLite?
KomarovI Автор
Я бы вообще не стал сравнивать BerkleyDB с SQLite. Berkley это не реляционная БД. В данном контексте было рассмотрено, как использовать ее в качестве реляционной, но это далеко не единственный и не главный кейс. Из преимуществ, если уж взяться сравнивать, следующее: репликация, вложенные транзакции, многопоточная работа, надежность, явность индексации, независимость от окружения (как в статье было написано — это встраиваемая СУБД), гибкая миграция. Куда более широкий спектр настроек, рассчитана на весьма солидные нагрузки и объемы данных.
upd: касательно API — соглашусь, во многом не очень удобен, но основные косяки можно закрыть своим кодом так же, как закрываем их в SQLite :)