Как происходит связь моделей с бд в java/scala приложениях?

Добрый день.

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

Сейчас пытаюсь влится в мир Java/Scala разработки. Я так понял, что здесь практикуется другой подход. Есть модели - простые классы, содержащие логику, а есть классы ORM, представляющие таблицы из базы. А как это связать вместе не понимаю.

Я разорался с синтаксисом Scala (с Java был поверхностно знаком). Решил сделать простой бложек. Развенлул приложение на PlayFramework2 (стандартный шаблон typesafe). С контроллерами, конфигами, вьюхами все понятно. А вот с моделями затык.

Добавил slick. Но нигде не могу найти нормального туториала. Пересмотрел куре реп на github, везде используются 3 разных подхода. В примерах на их сайте описана только работа с конструктором запросов (в разных ее проявлениях). Но как связать обычные классы-модели с бд, я понять не могу.

Пожалуйста, подскажите, где я могу найти описание основных подходов работы с бд через модели? Подойдет и java и scala. Мне главное понять саму суть. Или может покажете репозиторий с простым приложением, но в котором можно увидеть, как происходит работа с базой от начала и до конца?

Простите за сумбурный вопрос. Я просто в шоке. Все новое, много чего не понятно.
  • Вопрос задан
  • 5788 просмотров
Решения вопроса 4
@bromzh
Drugs-driven development
В мире Java есть много способов организации связи с БД и связи моделей с БД в частности. Есть стандарт - JPA. В мире spring есть слой совместимости с JPA, есть и иные решения. В Scala можно использовать и вышеупомянутые решения, и свои (тут уже зависит от используемого фреймворка).

Опишу, как это устроено в JPA.

Сперва ты описываешь связь всего приложения с БД в файле persistence.xml. В нём ты описываешь persistence-unit'ы - единицы персистентности, связь моделей с БД. Грубо говоря, можно использовать как локальные ресурсы (RESOURCE_LOCAL - связь происходит не через сервер приложений, а сторонними усилиями, связь описывается в xml-файле), так и ресурсы JTA (соединение настраивается на сервере, в приложении ты по имени получаешь нужный DataSource).

Потом ты создаёшь классы сущностей. Создаёшь обычный (POJO) класс с аннотацией Entity, описываешь поля, геттеры и сеттеры. Аннотациями можно настраивать всякие штуки: имя таблицы в БД, имя поля, задавать связи, тип получения (LAZY/EAGER), каскадность, автогенерацию первичных ключей и т.д.

Затем надо создать класс, который будет предоставлять интерфейс для работы с сущностями. Обычно, для каждой сущности надо сделать свой класс. Есть несколько вариантов реализации и названия этих классов. NetBeans, например, создаёт классы-фасады: один абстрактный и по-одному фасаду, наследующий абстрактный, на каждую сущность. По ссылке всё наглядно, я думаю. Каждый фасад - это бин (аннотация Stateless). В него инжектится EntityManager:
@PersistenceContext(unitName = "AffableBeanPU")
private EntityManager em;

При настроенной связи с БД (в persistence.xml) в em будет нужная реализация этого менеджера, через который и происходит вся магия. Плюсом является то, что все запросы автоматом используют транкзакции.

Ну а потом, в коде, надо просто инжектить этот фасад через аннотацию EJB и использовать его (например, для реализации REST API):
import org.foo.example.entities.Foo;
import org.foo.example.facades.FooFacade;

@Path("foo")
@Consumes({"application/json", "application/xml"})
@Produces({"application/json", "application/xml"})
class FooResource {

    @EJB
    FooFacade facade;

    @GET
    public List<Foo> getAll() {
        return facade.findAll();
    }

    @POST
    public Foo create(Foo item) {
        facade.create(item);
        return item;
    }

    @GET
    @Path("{id}")
    public Foo getOne(@PathParam("id") Integer id) {
        return facade.find(id);
    }

    @PUT
    @Path("{id}")
    public Foo update(@PathParam("id") Integer id, Foo item) {
        item.setId(id);
        facade.update(item);
        return item;
    }

    @DELETE
    @Path("{id}")
    public void delete(@PathParam("id") Integer id) {
        facade.remove(facade.find(id));
    }    
}


UPD.
Запилил демо-приложение, можешь взять посмотреть.

Суть такая: ставим wildfly, добавляем пользователя. Запускаем сервер. Можно зайти в админку 127.0.0.1:9990
Там на вкладке Configuration->Datasources будет 1 источник данных - ExampleDS. Это h2 - встроенная БД, которая крутится в данном случае в оперативке и при перезапуске сервера сбрасывается.

В файле persistence.xml настраиваем ресурсы: указываем имя persistence-unit, и его тип - JTA. Таким образом, ничего локально настраивать не надо, приложение получает всё через ресурс, который настроен на самом сервере, по его имени (java:jboss/datasources/ExampleDS). Единственное, в конфиге указываем
<property name="hibernate.hbm2ddl.auto" value="update" />
чтобы таблицы в БД автоматом создавались (если их нет).

В пакете entities лежат 2 сущности: User и Post. Обе аннотированны Entity, таким образом, JPA может с ними работать. Ещё в сущностях присутствуют аннотации для валидации сущностей (это всякие NotNull, Size, Min, Valid и т.д.). Так же, там есть простая двусторонняя связь. В сущности Post есть связь ManyToOne к сущности User, в сущности User есть связь OneToMany со списком постов пользователя. Последняя связь нужна, чтобы обеспечить каскадность на уровне JPA, но геттеров/сеттеров на неё нет, потому что я не хочу, чтобы этот список вылезал при получении пользователя. По-хорошему, надо её убрать, а в таблице post (которая связана с сущностью Post) надо самому прописать каскадность при удалении, потому что пользователь особо не должен знать, что у него в зависимых.

Далее, в пакете dao находятся классы для доступа к сущностям.
AbstractDao - это абстрактный generic-класс, в котором описаны операции для управления хранением сущностей. Все методы там тривиальны, за исключением получения.

Вообще, есть несколько способов получить сущность. Можно использовать простые SQL-запросы, можно указывать именованные запросы (NamedQuery), которые можно описать либо через одноимённую аннотацию, либо в коде. Вторые имеют бонус в виде типобезопасности.
И ещё один из вариантов - это динамические запросы через CriteriaBuilder. JPA генерирует метаклассы для классов, отмеченных аннотацией Entity. Запросы можно строить, в том числе, используя эти метаклассы. Большим плюсом является то, что такие запросы можно (и нужно) делать типобезопасными (CriteriaQuery, TypedQuery). И IDE не ругается на приведение типов, которое было бы в случае простых нетипизированных запросов. Вообще, по этой ссылке есть подробное описание таких типобезопасных запросов.

Особое внимание к методам findWithLazy и findAllWithLazy. В сущности Post поле owner помечено как связь ManyToOne с ленивым типом получения (fetch = FetchType.LAZY). Просто так такое поле получить трудно: ленивая загрузка работает только в пределах сессии: создалась сессия, запросились данные, сессия закрылась. И ленивые поля по-умолчанию не добавляются к возвращаемому объекту. Есть несколько способов побороть это. Можно убрать ленивость (fetch = FetchType.EAGER). Можно вызвать метод size() для поля-коллекции. Можно вручную получать поля. У меня сделано именно так. В методы findWithLazy и findAllWithLazy передаётся список полей, необходимых для получения. Я создаю запрос на получение корневого элемента: Root<T> root = criteriaQuery.from(entityClass);, а затем в цикле получаю необходимые поля: for (String field : fields) { root.fetch(field); }. При выполнении запроса эти поля присоединятся к результату.

В классах UserDao и PostDao я указываю аннотацию Stateless для CDI и реализую абстрактный метод getEntityManager() для получения экземпляра PersistenceManager. Сам экземпляр я внедряю через аннотацию PersistenceContext, где в качестве параметра unitName я указываю имя persistence-unit, которое обозначил в persistence.xml.

Ну и наконец, использование классов DAO в приложении (пакет resources). Я создаю простой REST API с помощью JAX-RS. Для каждой сущности создаю по своему ресурсу, в который внедряю через аннотацию EJB нужный DAO-класс. Там, думаю, всё очевидно.

В описании репозитория указано, как запустить и потестить всё это дело.

Надеюсь, всё понятно.
Ответ написан
Fesor
@Fesor
Full-stack developer (Symfony, Angular)
Добро пожаловать в чудесный мир Data-mapper, где все есть POJO и domain objects (считайте ваши модельки) полностью отделены от persistence layer (модельки ничего не знают о том как и кто их хранит).
Ответ написан
dimonz80
@dimonz80
Цитата из "Play for Scala"

Play’s original design was intended to support an alternative architecture, whose
model classes include business logic and persistence layer access with their data. This
“encapsulated model” style looks somewhat different from the Java EE style, as shown
in figure 3.5, and typically results in simpler code.
Despite all of this, Play doesn’t have much to do with your domain model. Play
doesn’t impose any constraints on your model, and the persistence API integration it
provides is optional. In the end, you should use whichever architectural style you prefer.


Play2 не навязывает ничего в плане организации бизнес-логики и хранения. Посмотрите примеры (к сожалению в дистре идут только с версиями до 2.2.х), они прозрачно намекают делать анемичной моделью case class, а БЛ и DAO пихать в объект-компаньон. Кортежи тупо мапяться в case class'ы модели и всё. См. пример computer-database
для Slick
и для Anorm

И да, все CRUD операции надо руками прописывать, хотя скафолдинг для DAO пишется запросто на голом JDBC как в сторону таблица -> класс, так и обратно. А можно воспользоваться чем нибудь готовым

И дался вам этот Slick? Чем людей anorm не устраивает...
Ответ написан
@kondaurov
Full stack developer
Развенлул приложение на PlayFramework2 (стандартный шаблон typesafe). С контроллерами, конфигами, вьюхами все понятно. А вот с моделями затык.


Я не совсем понимаю в чем у вас затык. Не знаете как реализовать получение строк из таблицы? Все очень просто!

https://www.playframework.com/documentation/2.3.6/... вам в помощь

В scala есть такая крутая штука как case class. Описание объектов этих кейс классов immutable, изменить можно только лишь скопировав объект с измененным полем (полями).

case class User(id: Option[Long] = None, name: String, password: String)
val a = User("John", "super")
val b = User.copy(name = "John2", password = "bla")


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

Почему immutable важно? Когда у меня есть коллекция кейс классов я знаю что никакие поля не изменятся в течение всего периода времени существования объектов этих кейс классов

Кейс классы знать нужно и уметь делать нужные, на все возможные случаи! Именно поэтому это case class а не forever_stable_structure class
Может показать что это не по теме но они используются везде, это главный друг scala программиста использующего play framework (да и не только, проекты на scala вообщем)

В пакете у меня несколько кейс классов для одной сущности. Нету времени на примеры

Получить запись пользователя из таблицы user по id можно так:

import anorm._

def getUserById(userId: Long): Option[User] = DB.withConnection { implicit r =>

SQL("SELECT * FROM users WHERE user_id = {userId}").on(
 'userId -> 
).map(r => User(
 "id" -> r[Option[Long]]("id"),
 "name" -> r[String]("name"),
 "password" -> r[String]("password")
)).singleOpt()

}


Или, во избежание дублирования кода маппинга полей таблицы в поля кейс класс, можно запихнуть этот код в объект компаньон кейс класса

package models.user.User

object User {

 def map(r: anorm.Row): User = User(
 "id" -> r[Option[Long]]("id"),
 "name" -> r[String]("name"),
 "password" -> r[String]("password")
)

}

case class User(id: Option[Long], name: String, password: String)


Потом создаем объект который я называю DAO. Он делает sql запрос и указывает какой функцией смапить строки отданные базой данных

//###########

package models.user.UsersDAO

import play.api.db.DB
import anorm._

def getUserById(id: Long): Option[User] = DB.withConnection { implicit r =>

SQL("SELECT * FROM users WHERE user_id = {userId}").on(
 'userId' -> id
).map(User.map).singleOpt()

}


Все очень просто и никаких затыков нету если ВНИМАТЕЛЬНО читать и стараться разобраться в сути а не делать так:
Я разорался с синтаксисом Scala (с Java был поверхностно знаком)


PS: Забыл упомянуть про Slick. Это такая штука которая избавляет от написания функций map(r: anorm.Row) и написания чистого SQL кода. В результате получается typesafe way в работе с получением данных от базы данных. Компилятор сразу компилирует мета код в чистый SQL и выплюнет ошибку если поле не правильно задано или тип не совпадает, тп. Это крутая вещь и с ней СТОИТ разобраться но сначала разберитесь с анормом, мне его пока хватает! Читайте мануалы по анорму, там все круто описано и я даже сам не знаю зачем все это написал когда сам во всем разобрался тем путем
Ответ написан
Комментировать
Пригласить эксперта
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Войти через центр авторизации
Похожие вопросы
Bell Integrator Ульяновск
До 400 000 ₽
Bell Integrator Хабаровск
До 400 000 ₽
Bell Integrator Ижевск
До 400 000 ₽