Видео-вариант:
Когда-то давно, в 2015 году, я опубликовал на Хабре статью про Java программу, если вкратце "Как я на коленке сделал свое 1С:Предприятие, с блек-джеком и шлюхами".
Вот на что это было похоже (каскад форм создается автоматически по классам Entities с помощью reflection):

Но это была, во-первых, двухзвенка, а во-вторых, потребовала от меня столько лапшекода, что после кодирования я надолго погрузился в депрессию. И до сих пор считаю, что при прототипировании, ручное вколачивание Entities, еще и в параллель со скриптами liquibase - дурацкая работа, в то время как "тупые 1Сники" (по мнению Настоящих Программистов), редактируют структуру БД вообще не лазя в код и не напрягаясь. Теперь же я готов представить вам вариант производственного процесса, который не отпугнет перебежчика с 1С на Java.
Ключевое преимущество у 1С:Предприятия - заучив десяток хоткеев ты можешь налабать учетную систему, вообще не прикасаясь к мышке, просто добавляя элементы в дерево объектов конфигуратора.
С вашего позволения, процитирую свою же статью:
"Проводя тренинги для РП, я ... с нуля делал простейший контур складского учета за 4 минуты 35 секунд ..., включая ввод в базу тестового примера из двух документов."
Даже сложнейшие 10+ уровневые SQL запросы в 1С создаются без кодирования, выбором списка полей в специальном мастере. Не говоря обо всем остальном. Поэтому тогда, в 2015, я был глубоко разочарован Enities классами, hibernate и прочими прелестями Java.
И вот, допустим, джуниор с 1Сным бэкграундом решил податься за длинным рублем в мир Java/Scala (псс, если что, у нас в банке имеется пара вакансий, пишите в личку).
Но его мозг, избалованный 1Сом, сопротивляется - еще вчера ему не нужно было делать кучу дурацкой работы по набиванию Pojo/Hibernate классов на клаве, и сколько такого волка не корми высоким окладом, он все равно рано или поздно постарается слинять в лес из этого царства BDSM.
Я попытаюсь доказать, что и в мире Java/Scala есть инструменты, которые позволяют создавать полноценные трехзвенные (Bloody Enterprise) приложения, прикладывая минимум усилий, и используя максимум шикарных помощников.
Для начала, запрототипируем БД. Для этого можно использовать практически любую СУБД и любой редактор. В моем случае это PostgreSQL и DBeaver, но если лень устанавливать СУБД, рекомендую использовать H2 database. Создаем там базу, и таблицы по своей задаче. Я создал таблицу tasks для классического to-do list примера.

Кто же создаст для нас дурацкие java Pojo классы? Никто, но скаловские case классы сущностей создаст команда sbt slickCodeGenTask. Вот что она натворила (хо��я смотреть не обязательно, вряд ли она накосячит):
package com.todolist.shared
// AUTO-GENERATED Slick data model
/** Stand-alone Slick data model for immediate use */
object Tables extends {
val profile = slick.jdbc.PostgresProfile
} with Tables
/** Slick data model trait for extension, choice of backend or usage in the cake pattern. (Make sure to initialize this late.) */
trait Tables {
val profile: slick.jdbc.JdbcProfile
import profile.api._
import slick.model.ForeignKeyAction
// NOTE: GetResult mappers for plain SQL are only generated for tables where Slick knows how to map the types of all columns.
import slick.jdbc.{GetResult => GR}
/** DDL for all tables. Call .create to execute. */
lazy val schema: profile.SchemaDescription = Tasks.schema
@deprecated("Use .schema instead of .ddl", "3.0")
def ddl = schema
/** Entity class storing rows of table Tasks
* @param id Database column id SqlType(serial), AutoInc, PrimaryKey
* @param text Database column text SqlType(varchar)
* @param done Database column done SqlType(bool) */
case class TasksRow(id: Int, text: String, done: Boolean)
/** GetResult implicit for fetching TasksRow objects using plain SQL queries */
implicit def GetResultTasksRow(implicit e0: GR[Int], e1: GR[String], e2: GR[Boolean]): GR[TasksRow] = GR{
prs => import prs._
TasksRow.tupled((<<[Int], <<[String], <<[Boolean]))
}
/** Table description of table tasks. Objects of this class serve as prototypes for rows in queries. */
class Tasks(_tableTag: Tag) extends profile.api.Table[TasksRow](_tableTag, "tasks") {
def * = (id, text, done) <> (TasksRow.tupled, TasksRow.unapply)
/** Maps whole row to an option. Useful for outer joins. */
def ? = ((Rep.Some(id), Rep.Some(text), Rep.Some(done))).shaped.<>({r=>import r._; _1.map(_=> TasksRow.tupled((_1.get, _2.get, _3.get)))}, (_:Any) => throw new Exception("Inserting into ? projection not supported."))
/** Database column id SqlType(serial), AutoInc, PrimaryKey */
val id: Rep[Int] = column[Int]("id", O.AutoInc, O.PrimaryKey)
/** Database column text SqlType(varchar) */
val text: Rep[String] = column[String]("text")
/** Database column done SqlType(bool) */
val done: Rep[Boolean] = column[Boolean]("done")
}
/** Collection-like TableQuery object for table Tasks */
lazy val Tasks = new TableQuery(tag => new Tasks(tag))
}
После того, как IDEA синхронизирует ваш проект, все сущности, которые подтянулись из SQL схемы, будут подсвечиваться и подсказываться по Ctrl + Space, в т.ч. в любом лямбда-выражении и for comprehension:

Что же в проекте предстоит закодировать нам лично? Всего 2 файла, сервера и клиента. Делать это мы будем, используя фреймворк Akka и концепцию акторов. Очень удобно, что акторы (клиент и сервер) могут относится к разным актор-системам и запускаться в разных концах интернета, обмениваясь сообщениями посредством TCP, UDP (могут и по HTTP/HTTPS, но об этом в другой раз).
Итак, код сервера:
package com.todolist
import akka.actor.{Actor, ActorRef, ActorSystem, Props}
import com.typesafe.config.ConfigFactory
import java.util.concurrent.Executors
import scala.concurrent.ExecutionContext
import scala.util.Success
object Server extends App{
val config =ConfigFactory.load("application")
val actorSystem = ActorSystem("serversystem", config)
val server: ActorRef = actorSystem.actorOf(Props[ToDoListServer],"todolistserver")
}
class ToDoListServer extends Actor{
import slick.driver.PostgresDriver.api._
implicit val db = Database.forConfig("my.myDb",Server.config)
implicit val ec = ExecutionContext.fromExecutorService(Executors.newFixedThreadPool(10))
override def receive: Receive = {
case x:String if (x.trim.isEmpty) =>
case x:String =>
val snd = sender()
val arr = x.split(' ')
arr(0) match {
case "list" =>
db.run(com.todolist.shared.Tables.Tasks.filter(_.done === false).sortBy(_.id).result)
.onComplete{ e => e match {
case Success(e) =>
val res = e.map(e=>s"${e.id}. ${e.text}").mkString("\n")
println(res)
snd ! res
}}
case "add" =>
db.run(com.todolist.shared.Tables.Tasks += com.todolist.shared.Tables.TasksRow(text = arr.drop(1).mkString(" "),done = false,id=0))
case "done" =>
val arr1 = arr(1).toInt
db.run((for { c <- com.todolist.shared.Tables.Tasks if c.id === arr1 } yield c.done).update(true))
case _ => sender() ! "Unknown command"
}
}
}
Как видите, никакого plain SQL - и одновременно, нет необходимости руками мапить структуру БД на case классы Entities.
Код клиента (вообще-то, я изначально скопировал его с какого-то примера из интернета, но так как ввод с клавы внутри актора глючил, от примера по сути ничего не осталось):
package com.todolist
import akka.actor.{Actor, ActorRef, ActorSystem, Props}
object Client {
case class Connect(server: ActorRef)
case class Process(cmd: String)
}
class Client extends Actor {
override def receive: Actor.Receive = {
case Client.Connect(server) =>
context become process(server)
}
def process(server: ActorRef): Receive = {
case Client.Process(cmd) =>
server ! cmd
case x:String =>
println(s"$x")
case _ =>
}
}
object ClientApp extends App {
import com.typesafe.config.ConfigFactory
import scala.concurrent.duration._
val actorSystem = ActorSystem("client-system", ConfigFactory.load("client"))
val client: ActorRef = actorSystem.actorOf(Props[Client])
val serverPath = "akka://serversystem@127.0.0.1:2552/user/todolistserver"
val serverSelection = actorSystem.actorSelection(serverPath)
serverSelection.resolveOne(FiniteDuration(10, SECONDS)).foreach { (server: ActorRef) =>
println(s"Connected to $server")
client ! Client.Connect(server)
while (true){
println("Enter command:")
scala.io.StdIn.readLine() match {
case cmd => client ! Client.Process(cmd)
}
}
} (actorSystem.dispatcher)
}
Актор-системы сервера и клиента, если они расположены на одном хосте, не могут висеть не одном и том же порту, поэтому, должны быть сконфигурированы разными файлами application.conf, client.conf. Кроме этого, для sbt придется вырубить jmx, там тоже происходит конфликт портов (либо, используйте разные конфиги и для sbt клиента и сервера).
Для запуска, в одном терминале выполняем:
sbt "runMain com.todolist.Server"
В другом:
sbt "runMain com.todolist.ClientApp"
Проверка:
Enter command: list
5. task2
7. task3
9. task5
Enter command: add task6
Enter command: list
5. task2
7. task3
9. task5
10. task6
Enter command: done 9
Enter command: list
5. task2
7. task3
10. task6
В данном, не побоюсь этого слова, трехзвенном приложении, я лично закодил не боле�� 80 строк; при этом, после создания схемы БД и автоматическом импорте ее в приложение, IDEA подсказывала мне, какие поля имеются у сущностей, и не налепил ли я ошибок. В принципе, это то, что делает конструктор запросов в 1С:Предприятии, поскольку ни строчки plain SQL я не написал. Но, конструктор запросов в 1С может делать только select`ы, все остальное предстоит делать самостоятельно - а тут, как видите, и Insert, и Update.
А вообще-то, в таком же стиле, на Slick вы можете закодить мега запросы для Spark SQL, которые распараллелятся и выполнятся на hadoop кластере из 1000 серверов, а потом обработать это все каким-нибудь XGBoost или Tensorflow. В общем, все только начинается...
