Telegram бот для персаналізаванай падборкі артыкулаў з Хабра

Для пытанняў у стылі "навошта?" ёсць больш стары артыкул Натуральны Geektimes - робім прастору чысцей.

Артыкулаў шмат, па суб'ектыўных прычынах некаторыя не падабаюцца, а некаторыя, наадварот, шкада прапускаць. Жадаецца аптымізаваць гэты працэс і эканоміць час.

У вышэйзгаданым артыкуле прапаноўваўся падыход са скрыптамі ў браўзэры, але ён мне не вельмі спадабаўся (хоць я ім і карыстаўся раней) па наступных прычынах:

  • Для розных браўзэраў на кампе/тэлефоне прыходзіцца наладжваць нанова, калі гэта наогул магчыма.
  • Жорсткая фільтраванне па аўтарах не заўсёды зручная.
  • Не вырашана праблема з аўтарамі, чые артыкулы не хочацца прапускаць, нават калі яны выходзяць раз на год.

Убудаваная ў сайт фільтраванне па рэйтынгу артыкулаў не заўсёды зручная, бо вузкаспецыялізаваныя артыкулы пры ўсёй іх каштоўнасці могуць атрымліваць даволі сціплы рэйтынг.

Першапачаткова я хацеў генераваць rss стужку (ці нават некалькі), пакідаючы там толькі цікавае. Але ў выніку атрымалася, што чытанне rss здалося не вельмі зручным: у любым выпадку для каментавання/галасавання за артыкул/даданні яе ў абранае даводзіцца заходзіць праз браўзэр. Таму я напісаў робата для тэлеграма, якія кідае мне ў твары цікавыя артыкулы. Тэлеграм сам па сабе робіць з іх прыгожыя превьюшки, што ў злучэнні з інфармацыяй аб аўтары / рэйтынгу / прагляду выглядае даволі інфарматыўна.

Telegram бот для персаналізаванай падборкі артыкулаў з Хабра

Пад катам падрабязнасці тыпу асаблівасцяў працы, працэсу напісання і тэхнічных рашэнняў.

Коратка аб боце

Рэпазітар: https://github.com/Kright/habrahabr_reader

Робат у тэлеграме: https://t.me/HabraFilterBot

Карыстальнік задае дадатковы рэйтынг для тэгаў і аўтараў. Пасля гэтага да артыкулаў ужываецца фільтр - складаюцца рэйтынг артыкула на Хабре, карыстацкі рэйтынг аўтара і сярэдняе для карыстацкіх рэйтынгаў па тэгах. Калі сума аказваецца больш зададзенага карыстальнікам парогавага значэння, то артыкул праходзіць фільтр.

Пабочнай мэтай напісання робата было атрыманне фана і досведу. Акрамя таго, я рэгулярна нагадваў сабе, што я - не гугл, а таму многія рэчы зроблены максімальна проста і нават прымітыўна. Зрэшты, гэта не перашкодзіла працэсу напісання робата расцягнуцца на тры месяцы.

За акном было лета

Сканчаўся ліпень, а я вырашыў напісаць бота. І не ў адзіночку, а са знаёмым, які асвойваў scala і хацеў што-небудзь напісаць на ёй. Пачатак выглядала шматабяцальным – код будзе пілаваць "камаднай", задача здавалася няцяжкай і я думаў, што праз пару тыдняў ці месяц робат будзе гатовы.

Нягледзячы на ​​тое, што я сам апошнія некалькі гадоў час ад часу пішу код на скале, гэты код звычайна ніхто не бачыць і не глядзіць: пет праекты, праверка нейкіх ідэй, перадапрацоўка дадзеных, засваенне нейкіх канцэпцый з ФП. Мне было рэальна цікава, як выглядае напісанне кода ў камандзе, таму што код на скале можна пісаць вельмі па-рознаму.

Што ж магло пайсці так? Зрэшты, не будзем прыспешваць падзеі.
Усё, што адбываецца, можна адсачыць па гісторыі коммітаў.

Знаёмы стварыў рэпазітрый 27 ліпеня, але больш нічога не зрабіў, а таму я пачаў пісаць код.

30 ліпеня

Коратка: я напісаў парсінг rss стужкі Хабра.

  • com.github.pureconfig для чытання typesafe канфігаў прамоў у case класы (аказалася вельмі зручна)
  • scala-xml для чытання xml: паколькі першапачаткова я хацеў напісаць сваю рэалізацыю для rss - стужкі, а rss стужка ў фармаце xml, то для парсінгу выкарыстаў гэтую бібліятэку. Уласна, парсінг RSS таксама з'явіўся.
  • scalatest для тэстаў. Нават для маленечкіх праектаў напісанне тэстаў эканоміць час - напрыклад, пры адладцы парсінгу xml нашмат прасцей запампаваць яго ў файлік, напісаць тэсты і паправіць памылкі. Калі ў далейшым з'явіўся баг з парсінгам нейкіх дзіўных html з неваліднымі utf-8 сімваламі, аказалася зноў жа зручней пакласці яго ў файлік і дадаць тэст.
  • акцёры з Akka. Аб'ектыўна, яны ўвогуле не былі патрэбныя, але праект пісаўся for fun, я хацеў іх паспрабаваць. У выніку гатовы сказаць, што мне спадабалася. На ідэю ААП можна зірнуць з іншага боку - ёсць акцёры, якія абменьваюцца паведамленнямі. Што цікавей - можна (і трэба) пісаць код з такім разлікам, што паведамленне можа не дайсці ці не быць апрацавана (наогул кажучы, пры працы акі на адным-адзіным кампе паведамлення не павінны губляцца). Я спачатку ламаў галаву і ў кодзе адбываўся трэш з падпіскамі акцёраў сябар на сябра, але ў выніку атрымалася прыйсці даволі простай і хупавай архітэктуры. Код усярэдзіне кожнага акцёра можна лічыць аднаструменным, пры падзеннях акцёра акка перазапускае яго — атрымліваецца даволі адмоваўстойлівая сістэма.

9 жніўня

Я дадаў у праект scala-scrapper для парсінгу html старонак з хабра (каб выцягваць інфармацыю тыпу рэйтынгу артыкула, колькасці дадання ў закладкі і да т.п.).

І Cats. Тыя самыя, якія ў скале.

Telegram бот для персаналізаванай падборкі артыкулаў з Хабра

Я тады чытаў адну кніжку пра размеркаваныя базы дадзеных, мне спадабалася ідэя CRDT (Conflict-free replicated data type, https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type, хабр), таму я запілаваў тайп-клас камутатыўнай паўгрупы для інфармацыі аб артыкулы на хабры.

Насамрэч, ідэя вельмі простая – у нас ёсць лічыльнікі, якія манатонна змяняюцца. Колькасць праматораў плаўна расце, колькасць плюсаў таксама (зрэшты, як і колькасць мінусаў). Калі ў мяне ёсць дзве версіі інфармацыі аб артыкуле, то можна іх «зліць у адну» — больш акутальнай лічыць той стан лічыльніка, які большы.

Паўгрупа абазначае, што два аб'екты з інфармацыяй аб артыкуле можна зліць у адзін. Камутытыўная азначае, што зліваць можна і А + B і B + A, вынік ад парадку не залежыць, у выніку застанецца найболей новая версія. Дарэчы, асацыятыўнасць тут таксама ёсць.

Напрыклад, па-задумцы, rss пасля парсінгу давала крыху аслебленую інфармацыю аб артыкуле - без метрык тыпу колькасці праглядаў. Адмысловы актор пасля гэтага браў інфармацыю аб артыкулах і бегаў да html старонак, каб яе абнавіць і зліць са старой версіяй.

Наогул кажучы, як і ў akka, у гэтым не было патрэбы, можна было проста для артыкула захоўваць updateDate і браць навейшую без усякіх зліццяў, але мяне вяла дарога прыгод.

12 жніўня

Я пачаў вальней сябе адчуваць і дзеля цікавасці зрабіў, каб кожны чат быў асобным актарам. Тэарэтычна, актар ​​сам па сабе важыць каля 300 байт і іх можна хоць мільёнамі ствараць, так што гэта нармальны падыход. Атрымалася, як мне падаецца, даволі цікавае рашэньне:

Адзін актор быў мастом паміж серверам тэлеграма і сістэмай паведамленняў у акцы. Ён проста атрымліваў паведамленні і адпраўляў іх патрэбнаму акцёру-чату. Акцёр-чат у адказ мог паслаць што-небудзь назад - і яно адпраўлялася назад у тэлеграм. Што было вельмі зручна – гэты актор атрымаўся максімальна простым і змяшчаў толькі логіку адказу на паведамленні. Дарэчы, інфармацыя аб новых артыкулах прыходзіла ў кожны чат, але я зноў жа не бачу ў гэтым ніякіх праблем.

Увогуле, робат ужо працаваў, адказваў на паведамленні, захоўваў спіс адпраўленых карыстачу артыкулаў і я ўжо думаў аб тым, што робат практычна гатовы. Я паціху дапілоўваў маленькія фішкі тыпу нармалізацыі імёнаў аўтараў і тэгаў (замяняў "s.d f" на "s_d_f").

Заставалася адно маленькае але - стан нікуды не захоўвалася.

Усё пайшло не так

Магчыма, вы заўважылі, што робата я пісаў пераважна адзін. Дык вось, другі ўдзельнік уключыўся ў распрацоўку, і ў кодзе аказаліся наступныя змены:

  • Для захоўвання стану з'явілася mongoDB. Заадно ў праекце паламаліся логі, таму што монга навошта-то пачынала ў іх спаміць і сёй-той іх проста глабальна выключыў.
  • Акцёр-мост у тэлеграм змяніўся да непазнавальнасці і пачаў сам парсіць паведамленні.
  • Акцёры для чатаў былі бязлітасна выпілаваны, замест іх з'явіўся актар, які хаваў у сабе ўсю інфармацыю аб усіх чатах адразу. На кожны чых гэты актор лез у монгу. Ну так, тыпу пры абнаўленні інфармацыі аб артыкуле адправіць яе ўсім актарам-чатам – цяжка (мы ж як гугл, мільёны карыстальнікаў так і чакаюць па мільёне артыкулаў у чат для кожнага), а вось пры кожным абнаўленні чата лезці ў монгу – гэта нармальна. Як я зразумеў значна пазней, якая працуе логіка працы чатаў таксама была цалкам выпілаваная і ўзамен з'явілася непрацуючае нешта.
  • Ад тайп-класаў не засталося і следа.
  • У акцёрах з'явілася нейкая нездаровая логіка з падпіскамі іх адзін на аднаго, якая вядзе да race condition.
  • Структуры дадзеных з палямі тыпу Option[Int] ператварыліся ў Int з магічнымі дэфолтнымі значэннямі тыпу -1. Пазней я зразумеў, што mongoDB захоўвае json і няма нічога дрэннага ў тым, каб захоўваць там Option ну ці хаця б парсіць -1 як None, але на той момант я гэтага не ведаў і паверыў на слова, што «так трэба». Той код пісаў не я, і я не лез яго мяняць да пары да часу.
  • Я даведаўся, што мой публічны айпі адрас мае ўласцівасць мяняцца, і кожны раз даводзілася дадаваць яго ў whitelist монге. Бота я запускаў лакальна, монга была дзесьці на серверах монгі як кампаніі.
  • Раптам знікла нармалізацыя тэгаў і фарматаванне паведамленняў для тэлеграма. (Хм, з чаго б гэта?)
  • Мне спадабалася, што стан робата захоўваецца ў знешняй БД, і пры перазапуску ён працягвае працаваць як ні ў чым ні бывала. Зрэшты, гэта быў адзіны плюс.

Другі чалавек не асабліва спяшаўся, і ўсе гэтыя змены з'явіліся адной вялікай кучай ужо ў пачатку верасня. Я не адразу ацаніў маштаб атрыманых разбурэнняў і пачаў разбірацца ў працы БД, т.я. раней і з імі не меў справу. Толькі потым я зразумеў, колькі працавальнага кода было выпілавана і колькі багаў дададзена ўзамен.

Верасня

Спачатку я думаў, што было б карысна асвоіць монгу і зрабіць усё добра. Потым я паціху пачаў разумець, што арганізаваць зносіны з бд - таксама мастацтва, у якім можна нарабіць гонак і проста памылак. Напрыклад, калі ад карыстальніка прыйдуць два паведамленні тыпу /subscribe - і мы ў адказ на кожнае створым па запісе ў таблічцы, таму што на момант апрацоўкі тых паведамленняў карыстач не падпісаны. У мяне ўзнікла падазрэнне, што зносіны з монгай ў існуючым выглядзе напісана не лепшым чынам. Напрыклад, настройкі карыстальніка ствараліся ў той момант, калі ён падпісваўся. Калі ён спрабаваў іх змяніць да факту падпіскі… робат нічога не адказваў, так як код у акцёры лез у аснову за наладамі, не шукаў і падаў. На пытанне – чаму б не ствараць наладкі па неабходнасці я даведаўся, што няма чаго іх мяняць, калі карыстальнік не падпісаўся… Сістэма фільтрацыі паведамленняў была зробленая неяк невідавочна, і я нават пасля пільнага погляду ў код не змог зразумець, было так задумана першапачаткова ці там памылка.

Спісу адпраўленых у чат артыкулаў не было, замест гэтага было прапанавана, каб я сам іх напісаў. Мяне гэта здзівіла - я ўвогуле-то быў не супраць зацягвання ў праект усялякіх штук, але было б лагічна ўцягнуў гэтыя штукі іх і прыкруціць. Але не, другі ўдзельнік, падобна, падзабіў на ўсё, але сказаў, што спіс усярэдзіне чата — нібы дрэннае рашэнне, і трэба зрабіць таблічку з івэнтамі тыпу «карыстачу x быў адпраўлены артыкул у». Потым, калі карыстач запытваў даслаць новыя артыкулы, трэба было адправіць запыт да бд, які з івэнтаў вылучыў бы івэнты, якія адносяцца да карыстача, яшчэ атрымаць спіс новых артыкулаў, адфільтраваць іх, адправіць карыстачу і накідаць івэнтаў пра гэта зваротна ў бд.

Другога ўдзельніка кудысьці панесла ў бок абстракцый, калі робату будуць прыходзіць не толькі артыкулы з Хабра і адпраўляцца не толькі ў тэлеграм.

Я неяк рэалізаваў івэнты ў выглядзе асобнай таблічкі да другой паловы верасьня. Неаптымальна, але робат хоць бы зарабіў і зноў пачаў дасылаць мне артыкулы, а я паволі разабраўся з тым, што адбываецца ў кодзе.

Цяпер можна вярнуцца спачатку і ўспомніць, што рэпазітар першапачаткова ствараў не я. Што ж магло пайсці так? Мой пул-рэквест быў адхілены. Аказалася, што ў мяне быдлакод, што я не ўмею працаваць у камандзе і я павінен быў кіраваць багі ў бягучай крывой рэалізацыі, а не дапрацоўваць яе да юзабельнага стану.

Я знерваваўся, паглядзеў гісторыю коммітаў, колькасць напісанага кода. Паглядзеў на моманты, якія першапачаткова былі напісаны добра, а потым зламаныя зваротна…

F*rk it

Я ўспомніў артыкул Вы - не Google.

Падумаў аб тым, што ідэя без рэалізацыі нікому асабліва не патрэбна. Падумаў аб тым, што я жадаю мець працавальнага робата, які будзе ў адным-адзіным асобніку працаваць на адным-адзіным кампутары як простая java-праграма. Я ведаю, што мой бот будзе працаваць месяцамі без перазапускаў, балазе ў мінулым я ўжо пісаў такіх робатаў. Калі ён раптам усёткі ўпадзе і не дашле карыстачу чарговы артыкул, неба не абрынецца на зямлю і нічога катастрафічнага не адбудзецца.

Навошта мне докер, mongoDB і іншы карго-культ "сур'ёзнага" софту, калі код тупа не працуе ці працуе крыва?

Я фаркнуў праект і зрабіў усё як хацеў.

Telegram бот для персаналізаванай падборкі артыкулаў з Хабра

Прыкладна тады ж я памяняў месца працы і вольнага часу стала катастрафічна не хапаць. Раніцай я прачынаўся роўна на электрычку, увечары вяртаўся позна і штосьці рабіць ужо не хацелася. Я нейкі час не рабіў нічога, потым жаданне дапісаць робата перадужала, і я стаў паволі перапісваць код, пакуль ездзіў на працу раніцай. Не скажу, што гэта было прадуктыўна: сядзець у якая калоціцца электрычцы з наўтбукам на каленях і падглядваць на stack overflow з тэлефона не вельмі зручна. Зрэшты, час за напісаннем кода пралятаў зусім неўзаметку, і праект пачаў паволі рухацца да працоўнага стану.

Дзесьці ў глыбіні душы быў чарвячок сумневу, які жадаў выкарыстаць mongoDB, але я падумаў, што акрамя плюсаў з "надзейным" захоўваннем стану ёсць прыкметныя мінусы:

  • БД становіцца яшчэ адной кропкай адмовы.
  • Код становіцца больш складаным, і пісаць яго я буду даўжэй.
  • Код становіцца павольным і неэфектыўным, замест змены аб'екта ў памяці змены адпраўляюцца ў БД і пры неабходнасці выцягваюцца зваротна.
  • З'яўляюцца абмежаванні тыпу захоўвання івентаў у асобнай шыльдзе, якія звязаны з асаблівасцямі працы БД.
  • У трыяльнай версіі монгі ёсць нейкія абмежаванні, і калі ў іх уперціся, давядзецца на чымсьці монгу запускаць і наладжваць.

Выпілаваў монгу, зараз стан робата проста захоўваецца ў памяці праграмы і час ад часу захоўваецца ў файлік у выглядзе json. Магчыма, у каментарах напішуць, што я не мае рацыю, бд менавіта тут трэба выкарыстоўваць і да т.п. Але гэта мой праект, падыход з файлам максімальна просты і ён працуе празрыстай выявай.

Выкінуў магічныя значэнні тыпу -1 і вярнуў нармальныя Option, дадаў захоўванне хэш-таблічкі з адпраўленымі артыкуламі назад у аб'ект з інфармацыяй аб чаце. Дадаў выдаленне інфармацыі аб артыкулах старэйшых за пяць дзён, каб не захоўваць усё запар. Прывёў лагіраванне да працоўнага стану - логі ў разумных колькасцях пішуцца і ў файл і ў кансоль. Дадаў некалькі адмінскіх каманд тыпу захавання стану або атрыманні статыстыкі тыпу колькасці карыстачоў і артыкулаў.

Выправіў кучу дробязяў: напрыклад, для артыкулаў зараз паказваецца колькасць праглядаў, лайкаў-дызлайкаў і каментароў на момант праходжання фільтра карыстальніка. Наогул, дзіўна, колькі дробязяў прыйшлося паправіць. Я вёў спісачак, адзначаў там усе "шурпатасці" і па меры магчымасцяў выпраўляў іх.

Напрыклад, я дадаў магчымасць прама ў адным паведамленні задаць усе наладкі:

/subscribe
/rating +20
/author a -30
/author s -20
/author p +9000
/tag scala 20
/tag akka 50

І яшчэ каманда /settings выводзіць іх менавіта ў такім выглядзе, можна браць тэкст ад яе і адпраўляць усе наладкі аднаму.
Накшталт і дробязь, але падобных нюансаў - дзясяткі.

Рэалізаваў фільтраванне артыкулаў у выглядзе простай лінейнай мадэлі - карыстач можа задаць дадатковы рэйтынг аўтарам і тэгам, а гэтак жа парогавае значэнне. Калі сума рэйтынгу аўтара, сярэдняга рэйтынгу для тэгаў і рэальнага рэйтынгу артыкула апынецца больш парогавага значэння, то артыкул паказваецца карыстачу. Можна або прасіць у робата артыкула камандай /new, або падпісацца на робата і ён будзе кідаць артыкулы ў твары ў любы час сутак.

Наогул кажучы, у мяне была ідэя для кожнага артыкула выцягнуць больш прыкмет (хабы, колькасць каментароў, даданняў у закладкі, дынаміку змены рэйтынгу, колькасць тэксту, малюначкаў і кода ў артыкуле, ключавыя словы), а карыстачу паказваць галасавалку ок/не ок пад кожнай артыкулам і пад кожнага карыстальніка навучаць мадэль, але мне стала лянота.

У дадатак, логіка працы стане не такой відавочнай. Цяпер я магу ўручную паставіць для patientZero рэйтынг 9000 і пры парогавым рэйтынгу ў 20 буду гарантавана атрымліваць усе яго артыкулы (калі, вядома, не пастаўлю -100500 для якіх небудзь тэгаў).

Выніковая архітэктура атрымалася даволі простай:

  1. Акцёр, які захоўвае стан усіх чатаў і артыкулаў. Ён грузіць свой стан з файліка на дыску і час ад часу захоўвае яго назад, кожны раз у новы файлік.
  2. Акцёр, які час ад часу набягае ў rss-стужку, даведваецца аб новых артыкулах, зазірае па спасылках, парсіт, і апраўляе гэтыя артыкулы першаму акцёру. Акрамя таго, ён часам запытвае ў першага акцёра спіс артыкулаў, выбірае тыя з іх, якія не старэйшыя за тры дні, але пры гэтым даўно не абнаўляліся, і абнаўляе іх.
  3. Акцёр, які мае зносіны з тэлеграмам. Я ўсё ж вынес парсінг паведамленняў цалкам сюды. Па-добраму хочацца падзяліць яго на два - каб адзін парсіў ўваходныя паведамленні, а другі займаўся транспартнымі праблемамі тыпу пераадпраўкі паведамленняў, якія не адправіліся. Цяпер пераадпраўкі няма, і паведамленне, якое не дайшло з-за памылкі, проста згубіцца (хіба што ў логах адзначыцца), але пакуль што гэта не выклікае праблем. Магчыма, праблемы ўзнікнуць, калі на робата падпішацца куча чалавек і я дасягну ліміту на адпраўку паведамленняў).

Што мне спадабалася – дзякуючы akka падзення акцёраў 2 і 3 увогуле-то не ўплываюць на працаздольнасць робата. Магчыма, нейкія артыкулы не абнаўляюцца своечасова ці нейкія паведамленні не даходзяць да тэлеграма, але акка перазапускае актор і ўсё працягвае працаваць далей. Я захоўваю інфармацыю аб тым, што артыкул паказаны карыстачу толькі тады, калі тэлеграм актор адкажа, што ён паспяхова даставіў паведамленне. Самае страшнае, што мне пагражае - адправіць паведамленне некалькі разоў (калі яно даставіцца, але пацвярджэнне нейкім невядомым чынам згубіцца). У прынцыпе, калі б першы актор не захоўваў стан у сабе, а меў зносіны з якой-небудзь бд, то ён мог бы таксама неўзаметку падаць і вяртацца да жыцця. Яшчэ я мог бы паспрабаваць akka persistance для аднаўлення стану акцёраў, але бягучая рэалізацыя мяне задавальняе сваёй прастатой. Не тое каб мой код часта падаў - наадварот, я прыклаў даволі шмат намаганняў, каб гэта было немагчымым. Але shit happens, і магчымасць разбіць праграму на ізаляваныя кавалачкі-акцёры здалася мне рэальна зручнай і практычнай.

Дадаў circle-ci для таго, каб пры паломцы кода адразу аб гэтым даведвацца. Як мінімум, аб тым, што код перастаў кампілявацца. Першапачаткова хацеў дадаць travis, але ён паказваў толькі мае праекты без форкнутых. Увогуле-тое абедзве гэтыя штукі можна вольна выкарыстоўваць на адкрытых рэпазітарах.

Вынікі

Ужо лістапад. Бот напісаны, я карыстаўся ім апошнія два тыдні і мне спадабалася. Калі ёсць ідэі па паляпшэнні - пішыце. Я не бачу змыла манетызаваць яго - няхай проста працуе і дасылае цікавыя артыкулы.

Спасылка на бота: https://t.me/HabraFilterBot
Гітхаб: https://github.com/Kright/habrahabr_reader

Невялікія высновы:

  • Нават маленькі праект можа моцна зацягнуцца па часе.
  • Вы - не гугл. Няма сэнсу страляць з гарматы па вераб'ях. Простае рашэнне можа працаваць не горш.
  • Пэт-праекты вельмі добра падыходзяць для эксперыментаў з новымі тэхналогіямі.
  • Тэлеграм боты пішуцца даволі проста. Калі б не "камандная праца" і эксперыменты з тэхналогіямі, робат быў бы напісаны за тыдзень-два.
  • Мадэль акцёраў - цікавая штука, добра якая спалучаецца са шматструменнасцю і адмоваўстойлівасцю кода.
  • Здаецца, я адчуў на сабе, чаму open source супольнасць кахае форкі.
  • Базы дадзеных добрыя тым, што стан прыкладання перастае залежаць ад падзенняў/перазапускаў прыкладання, але праца з БД ускладняе код і накладвае абмежаванні на структуру дадзеных.

Крыніца: habr.com

Дадаць каментар