Введение


Немного о сабже. BerkleyDB — высокопроизводительная встраиваемая СУБД, поставляемая в виде библиотеки для различных языков программирования. Это решение предполагает хранение пар ключ-значение, также поддерживается возможность ставить одному ключу в соответствие несколько значений. BerkeleyDB поддерживает работу в многопоточной среде, репликацию, и многое другое. Внимание данной статьи будет обращено в первую очередь в сторону использования библиотеки, предоставленной Sleepycat Software в бородатых 90х.

В предыдущей статье мы рассмотрели основные аспекты работы с Direct Persistence Layer API, благодаря которому можно работать с Berkeley как с реляционной БД. Сегодня же внимание будет обращено в сторону Collections API, которое предоставляет возможность работы через всем привычные Java Collections интерфейс-адаптеры.

Примечание: все примеры в данной статье будут приведены на языке Kotlin.

Немного общей информации


Все мы знаем, что работа с аннотациями, отложенной инициализацией и nullable типами в Kotlin это большая-большая боль. В силу своей специфики, DPL не позволяет эти проблемы устранить, и единственной лазейкой является создание своей реализации EntityModel — механизма, определяющего способ работы с бинами. Основной стимул, лично для меня, использовать Collections API, это возможность работы с полностью чистыми, привычными data-class бинами. Давайте рассмотрим, как перенести код из предыдущей статьи на этот фреймворк.

Описание сущностей


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

К сущности требований не предъявляется, а ключ и значение (при стандартной работе с БД, которую покрывает данная статья) обязаны реализовывать интерфейс Serializable. Здесь все стандартно, хотим in-memory поля — добавляем к ним аннотацию @Transient. Все, что не помечено как @Transient будет сериализовано.

Как мы помним, для упорядочивания записей в выборке требуется использовать в качестве ключа реализацию интерфейса Comparable. Здесь принцип тот же: выборка будет отсортирована по ключам.

Пример описания бина
data class CustomerDBO(
        val id: String,
        val email: String,
        val country: String,
        val city: String,
        var balance: Long
)

data class CustomerKey(
        val id: String
): Serializable

data class CustomerData(
        val email: String,
        val country: String,
        val city: String,
        val balance: Long
): Serializable


Операции над сущностями


Для создания связи в случае Collections API придется немного попотеть. Для начала следует рассмотреть принцип работы в самом распространенном случае — N:1.

Стандартные действия по созданию EnvironmentConfig опустим, поскольку он ничем принципиально не отличаются от конфигурации для DPL. Различия начинаются сразу после них.

Для каждой из сущностей мы должны создать отдельную базу данных, дав ей уникальное название, плюс, требуется создать отдельную базу данных, которая хранит информацию о сущностях в данном Environment и обернуть ее в ClassCatalog. Можно сказать, что в Berkeley базы данных имеют примерно ту же суть, что и таблицы в SQL. Пример под катом.

Создание базы данных для сущности и каталога
    private val databaseConfig by lazy {
        DatabaseConfig().apply {
            transactional = true
            allowCreate = true
        }
    }

    private val catalog by lazy {
        StoredClassCatalog(environment.openDatabase(null, STORAGE_CLASS_CATALOG, databaseConfig))
    }

    val customersDatabase by lazy {
        environment.openDatabase(null, STORAGE_CUSTOMERS, databaseConfig)
    }


Далее, логично, что нам требуется какая-то удобная для использования точка контакта с фреймворком, поскольку сама Database имеет весьма скверный низкоуровневый API. Такими адаптерами являются классы StoredSortedMap и StoredValueSet. Удобнее всего первый использовать в качестве иммутабельной точки соприкосновения с БД, а второй — мутабельной.

Collection adapters

    private val view = StoredSortedMap<CustomerKey, CustomerData>(
           customersDatabase,
           customerKeyBinding,
           customerDataBinding,
           false
    )

    private val accessor = StoredValueSet<CustomerDBO>(
           customersDatabase,
           customerBinding,
           true
    )


Можно заметить, что на данный момент, Berkeley не знает, каким образом осуществляется маппинг (key, data) -> (dbo) и dbo -> (key, data). Для того, чтобы маппинг работал, требуется реализовать еще по одному механизму для каждого из бинов — binding-у. Интерфейс предельно прост — два метода, для маппинга в данные и в ключ, и один — в сущность.

Пример binding-а
class CustomerBinding(
        catalog: ClassCatalog
): SerialSerialBinding<CustomerKey, CustomerData, CustomerDBO>(catalog, CustomerKey::class.java, CustomerData::class.java) {

    override fun entryToObject(key: CustomerKey, data: CustomerData): CustomerDBO = CustomerDBO(
            id = key.id,
            email = data.email,
            country = data.country,
            city = data.city,
            balance = data.balance
    )

    override fun objectToData(dbo: CustomerDBO): CustomerData = CustomerData(
            email = dbo.email,
            country = dbo.country,
            city = dbo.city,
            balance = dbo.balance
    )

    override fun objectToKey(dbo: CustomerDBO): CustomerKey = CustomerKey(
            id = dbo.id
    )
}


Теперь мы можем смело использовать работающие коллекции, которые будут автоматически синхронизироваться по мере изменения данных в БД. При этом «мякоткой» является возможность преспокойно использовать параллельную запись и чтение из разных потоков. Эта возможность обусловлена прежде всего тем, что iterator этих коллекций будет копией текущего состояния, и при изменениях коллекции меняться не будет, в то время, как сами коллекции — мутабельны. Таким образом, единственное, о чем следует задумываться программисту — контроль актуальности данных.

Ну что же, с обычным CRUD мы разобрались, переходим к связям!

Связи между сущностями


Для работы со связями, нам потребуется дополнительно создать SecondaryDatabase, которая будет предоставлять доступ к одним сущностям по ключу других. Важным замечанием является необходимость указать в DatabaseConfig значение sortedDuplicates как true, если связь не является 1:1 или 1:M. Это действие довольно логично, исходя из того, что индексация будет происходить по foreign key, и одному ключу будет соответствовать несколько сущностей.

Пример вторичной БД с конфигурацией
    val ordersByCustomerIdDatabase by lazy {
        environment.openSecondaryDatabase(null, STORAGE_ORDERS_BY_CUSTOMER_ID, ordersDatabase, SecondaryConfig().apply {
            transactional = true
            allowCreate = true
            sortedDuplicates = true
            keyCreator = OrderByCustomerKeyCreator(catalog = catalog)
            foreignKeyDatabase = customersDatabase
            foreignKeyDeleteAction = ForeignKeyDeleteAction.CASCADE
        })
    }


Примечательно, что в качестве внешнего ключа можно выбрать не только поле, по которому будет выставляться связь, но и любой произвольный тип данных. Роль создания ключей берет на себя реализация интерфейса SecondaryKeyCreator, или SecondaryMultiKeyCreator (присутствуют и более специфичные опции, но достаточно — реализовать один из этих двух).

Пример SecondaryKeyCreator
class OrderByCustomerKeyCreator(
        catalog: ClassCatalog
): SerialSerialKeyCreator<OrderKey, OrderData, CustomerKey>(catalog, OrderKey::class.java, OrderData::class.java, CustomerKey::class.java) {

    override fun createSecondaryKey(key: OrderKey,
                                    data: OrderData): CustomerKey = CustomerKey(
            id = data.customerId
    )
}


Осталось чуть-чуть — создать коллекцию для получения выборок по нашим внешним ключам, код под катом, от создания коллекция для не-вторичных БД ничем принципиально не отличается.

Создание коллекции для осуществления выборок N:1
private val byCustomerKeyView = StoredSortedMap<CustomerKey, OrderData>(
            database.ordersByCustomerIdDatabase,
            database.customerKeyBinding,
            database.orderDataBinding,
            false
    )


Вместо заключения


Эта статья была последней из части цикла, посвященного знакомству с основами работы с BerkeleyDB. После прочтения этой и предыдущей статьи, читатель способен использовать СУБД в своих проектах в качестве локального хранилища, например — в клиентском приложении. В следующих статьях будут рассмотрены более интересные аспекты — миграция, репликация, некоторые интересные параметры конфигурации.

Как обычно — если у кого-то есть дополнения или исправления моих косяков — милости прошу в комментарии. Я всегда рад конструктивной критике!

Комментарии (0)