scala: czytanie tabel

scala: czytanie tabel
Julian_
  • Rejestracja:około 8 lat
  • Ostatnio:ponad 4 lata
  • Postów:1703
0

Jak scalą pobieracie tabele z bazy?
Jak to zrobić bardziej scalowo?

Kopiuj

  classOf[org.postgresql.Driver]
  val table = "dbo.countries"

  override def findAll: Seq[CountriesDto] = {
    var rows = ListBuffer[CountriesDto]()
    try {
      val sql = "SELECT * FROM ?"
      val stmt = conn.prepareStatement(sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY)
      stmt.setString(1, table)
      val rs = stmt.executeQuery()
      while (rs.next) {
        rows += CountriesDto(rs.getString(0), rs.getString(1), rs.getBoolean(2))
      }
    }
    finally {
      conn.close()
    }
    rows.toSeq
  }

Podoba mi się rozwiązanie sparkownikowe, ale nie chcę sparka zaciągać tylko do tego.

Kopiuj
spark.read
  .jdbc("jdbc:postgresql:dbserver", table, conn)
edytowany 1x, ostatnio: Julian_
DQ
  • Rejestracja:prawie 10 lat
  • Ostatnio:6 miesięcy
  • Postów:141
2

Użyj jakiejkolwiek z powszechnie używanych bibliotek:

Czego nie wybierzesz to będzie "bardziej scalowe" i po prostu ładniejsze. Ręczne rzeźbienie tego co ktoś już zrobił nie ma sensu.

Julian_
no tylko nie wiem którą wybrać właśnie... a może da się ten zwykły jdbc co napisałem jakoś upiększyć
DQ
Mimo wszystko polecałbym zapoznanie się z jakąś biblioteką skoro chcesz sobie klepać w Scali, zwróci się w przyszłości. Zapewne najłatwiej Ci będzie z scalikejdbc bo jego API jest dość proste i powinno być w miarę podobne do tego co aktualnie już znasz. Jeśli chcesz troszkę bardziej "scalowo" ale nadal dość prosto to Quill. Slick może być troszkę bardziej wymagający, ale ma książkę dość dobrze tłumaczącą co i jak. Doobie/Skunk są trudniejszymi bibliotekami, wymagającymi znajomości FP i ogólnie ekosystemu Scali.
semicolon
  • Rejestracja:ponad 5 lat
  • Ostatnio:prawie 5 lat
  • Postów:114
2

JDBC sam w sobie nie jest zły. Do prostych rzeczy wystarczy, a do złożonych jest najlepszy.

W Twoim przypadku próbowałbym rozdzielić kod budujący stmt od kodu, który skanuje wynik.

Nie mniej sam JDBC będzie sprawiał Ci większe kłopoty, gdy będziesz chciał robić coraz więcej zadań:

  • budować zapytania z dynamicznie zmieniającymi się kryteriami
  • gdy będziesz robić powtarzalne złączenia, warunki, unie
  • gdy w projekcie będziesz miał dużo tabel i gdy często będziesz je zmieniać

Dobry mapper takie rzeczy wychwyci dość sprawnie, ale sam w sobie utrudni Ci budowanie złożonych zapytań, czasem będziesz musiał obchodzić na około to co normalnie mógłbyś wyciągnąć używając jdbc.

Nie znam najlepszej odpowiedzi na to pytanie, ale wiem, że docelowo możesz:

  • łączyć oba podejścia
  • uprzeć się jak osioł i iść w samo jdbc o ile pokryjesz zapytania testami
  • można zepchnąć zapytania z bazą na język, który ma najlepiej rozwinięty ORM
  • rozważyć inną bazę lub nawet brak bazy :D
DQ
Ale po co komu ORM w 2020 ;) Dobrze zaprojektowana biblioteka daje możliwości jak i JDBC (bądź większe, patrz Skunk), a jednocześnie w 99.99% będzie to po prostu lepsze (pod względem czytelności i rozwijalności) rozwiązanie
semicolon
a czemu nie? proszę weź napisz union w tym skunku
semicolon
@Wibowit: nie ten lib, prosiłem o przykład w skunku
semicolon
@Wibowit: No, ale skoro wybrałeś slick to mam inne pytanie, post niżej:
DQ
Skunk jest w chwili obecnej w mocnej alphie więc pewnie po prostu się jeszcze nie da ;) Nie mniej, jest to bardzo ciekawa biblioteka chociażby z tego względu, że jest nonblocking i nie korzysta z JDBC.
semicolon
jest nonblocking i nie korzysta z JDBC i to jest ten rok 2020? :-)
DQ
W jakiś sposób tak, nadal większość bibliotek związanych z relacyjnymi bazami danych pod spodem ciśnie na JDBC. Tutaj masz zaimplementowany protokół komunikacji z Postgresem od zera. Co do joinów to jednak wygląda na to, że się da: https://github.com/tpolecat/skunk/blob/master/modules/example/src/main/scala/Join.scala. Syntax jest jeszcze dość mocno niskopoziomowy ale to pewnie się zmieni wraz z rozwojem
Wibowit
JDBC to także przyszłość, bo dzięki Project Loom stanie się nieblokujące (w sensie nie będzie blokować wątków tych na poziomie systemu operacyjnego): https://www.oracle.com/a/tech/docs/dev6323-reactivestreams-fiber.pdf
semicolon
To jest rodzaj optymalizacji, ale kiedy on ma znaczenie jeśli wąskim gardłem na ogół jest baza?
Wibowit
Ma znaczenie jeśli chcesz uruchomić więcej zapytań naraz niż rzeczywistych wątków. Każdy wątek trochę zasobów zajmuje.
semicolon
Hmm.. po co ktoś miałby odpalać więcej zapytań niż jest w stanie obsłużyć? Przecież po drugiej stronie baza też ma pulę - ona też ma limit.
Wibowit
A to baza tworzy osobny wątek na każde połączenie do bazy? Jeśli tak to JDBC cały czas jest 100% OK, no nie?
semicolon
W pytaniu miałem na myśli sytuację gdy puszczasz N zapytań asynchronicznie w ramach tego samego połączenia, wtedy to N-1 zapytań leżakuje. Jaka korzyść z tego?
DQ
To nie jest optymalizacja tylko sposób pisania aplikacji. Jeśli decydujesz się na non-blocking to nie robisz tego w jednym miejscu (baza) tylko w całej aplikacji. Inaczej to nie ma większego sensu bo co Ci z tego, że masz non-blocking po stronie bazy jak potem blokujesz wątek bo na coś musisz poczekać. Jeśli decydujesz się na reaktywny system to rozlewa się on wszędzie. Aktualnie musisz się bawić w specjalne pule wątków żeby osiągnąć coś na kształt "non-blocking" ale i to ma swoje problemy. Dlatego biblioteka non-blocking do relacyjnej bazy jest dość innowacyjna
semicolon
Ach.. łapie, mój błąd. Każde zapytanie leci osobnym połączeniem.
semicolon
co Ci z tego, że masz non-blocking po stronie bazy jak potem blokujesz wątek bo na coś musisz poczekać - nie musisz mieć asynchronicznego api, by takie rzeczy robić z powodzeniem, wystarczy blokujące rzeczy wykonać przez osobny executor.
semicolon
@Wibowit: jak masz N połączeń do bazy, i wszystkie używasz z poziomu asynchronicznego, to co dzieje się z nadmiarowymi żądaniami? czy to nie jest tak, że pętla zdarzeń je przetrzymuje w pamięci i też uwaga: nie obsługuje?
DQ
Tak, możesz wykonywać blokujące rzeczy przez osobny executor ale to jest zasłanianie problemu, a nie jego rozwiązanie. Niby fajnie, że będziesz miał pulę 8 czy 16 wątków działających w sposób non-blocking (zapewne główna logika aplikacji), tylko co z tego, skoro obok tego wyrasta pula 100 czy 200 wątków blokujących i oczekujących na rezultat/połączenie z/do bazy co wpłynie na wydajność całego rozwiązania. Dlatego też, możliwość wykonywania zapytań w sposób non-blocking jest czymś "nowym", zwyczajnie tego jeszcze nie ma w standardach
Wibowit
@semicolon: A to w ogóle java.sql.Connection jest thread-safe? Jakoś nie widziałem robienia kilku współbieżnych zapytań na jednym połączeniu. Zwykle się robi pulę połączeń i jeśli jakiś wątek chce zrobić nowe zapytanie to wyciąga połączenie z puli i ma go dla siebie aż do momentu gdy go odda do puli.
semicolon
@Wibowit Ale jak masz N połączeń (pula), każde zajmiesz zadaniem to w czym asynchroniczność Ci pomaga? Masz conajwyżej więcej połączeń po stronie aplikacji, bez konieczności posiadania wielu wątków (tu mniej pamięci Ci zjada i context switching mniej boli), ale jeśli asynchronicznie przyjmiesz więcej żądań to one będą buforowane w pętli zdarzeń (tu tracisz pamięć) - jakby nie patrzeć zadania będą leżeć - co w ten sposób zyskasz?
semicolon
@DisQ - docelowo chcesz powiedzieć, że więcej zapytań odpalisz czy, że oszczędzisz pamięć na wątkach?
Wibowit
@semicolon: A w czym ma pomagać lub nie, bo nie rozumiem problemu? Project Loom dostarcza lekkie wątki, które można (w miarę) swodobnie blokować, bo nie blokują rzeczywistych (ciężkich) wątków systemu operacyjnego. Natomiast jeśli rozmawiamy o serwerze HTTP to tutaj robisz reactive stream, czyli serwer blokuje zapytania (otwiera połączenia, ale wstrzymuje od razu ich przetwarzanie) dopóki nie zwolnią się zasoby i wtedy klient HTTP musi czekać nawet na to, by np wysłać payload POSTem.
semicolon
Zrozumiałem, że główny motyw w przypadku asynchronicznego jdbc to taki, że można więcej zapytań odpalić. Tymczasem to nie jest rzecz nie znająca limitów, limity są i są one narzucone przez bazę. Natomiast jeśli docelowo chodzi o ograniczenie kosztów związanych z pamięcią operacyjną (bo wątki ze strony aplikacji to też koszt) to już łapie o co wam chodziło.
Wibowit
Jeśli sobie założymy, że n.p. 20 jednoczesnych zapytań/ połączeń/ etc wystarczy każdemu to nawet synchroniczne JDBC było, jest i będzie dobrym rozwiązaniem. Tak czy nie?
DQ
@semicolon: zapytań więcej nie odpalisz bo bazy mają swoje ograniczenia. Do postgresa można otworzyć chyba 100 połączeń i z tego co wiem, to aktualnie tego się nie przeskoczy. Oszczędność może być w zasobach gdzie nie tworzysz/reużywasz wątków tylko po to, żeby sobie wisiały i czekały na możliwość wykonania zapytania / odpowiedź z bazy danych. Brak dodatkowych 100 wątków, które w czasie swojego życia zdecydowaną większość czasu po prostu czekają oznacza oszczędność pamięci czy CPU, większą wydajność czy mniejsze rachunki na AWSie
semicolon
Łapie do czego zmierzacie, ale to nie jest zdrowe. Przy małej ilości połączeń taka asynchroniczność to narzut: https://techspot.zzzeek.org/2015/02/15/asynchronous-python-and-databases/ Przy dużej ilości połączeń OK oszczędzacie RAM, ale wtedy to jest powód by martwić się o bazę. Asynchroniczność to też problemy (np. musisz bardziej obsługiwać monady niż wyjątki, które są bardziej naturalne w jvm), pogodzić to z istniejącymi bibliotekami, trzeba mieć naprawdę skłonność do walki z wiatrakami. Ja jej nie mam :-/
semicolon
Nie mam doświadczenia z nie wiadomo jak wielkimi projektami. Tutaj bardziej stosowalbym asynchroniczność tam gdzie jest potrzebna. Potrzebne jest podtrzymanie połączenia z websocketem? OK - asynchronicznośc. Potrzebne jest intensywne pukanie w API - ok asynchroniczność, ale te rzeczy to dla mnie margines, łatwe do wyodrębnienia (jako osobna usługa) i dlatego główna część apki nadal prowadziłbym synchronicznie.
Wibowit
Czyli synchroniczne JDBC jest w takim razie OK dla ciebie. Wyjątki natomiast działają normalnie w przypadku virtual threads z Project Loom, widać pełen stacktrace, działa try/catch/finally, debugger itp itd Cała ta zabawa w Project Loom jest właśnie po to, by kod został w dużej mierze taki jaki jest, a nie był usiany async, await i Future'ami na każdym kroku (względnie Taskami czy jak to tam sobie ktoś lubi nazwać).
semicolon
Spodziewałem się, że napiszesz, że korpo firmy mają innej rangi problemy, i takie tam. Faktem faktem wątpie, bym miał kiedyś takie problemy. Po prostu byłem ciekaw tego zachwytu nad asynchronicznym dostępem do jdbc. W każdym razie dzięki za komentarze :)
Wibowit
Po prostu cały czas zastanawiam się co miał znaczyć komentarz jest nonblocking i nie korzysta z JDBC i to jest ten rok 2020? :-) - semicolon dziś, 10:52 ? W Project Loom zwykły blokujący kod JDBC staje się nieblokujący, gdy odpali się go na Virtual Thread, tzn będzie blokował lekki wirtualny wątek, zamiast tego ciężkiego rzeczywistego, a takie blokowanie nie jest już problemem (wirtualnych wątków można napykać miliony, rzeczywiste trzeba trzymać w małej puli).
semicolon
Byłem ciekaw czemu użycie ORM w 2020 to zły pomysł.
Wibowit
Bo jest niewypałem? Pomiędzy JDBC, a ORM są właśnie takie wynalazki jakie wymienił @DisQ w poście powyżej (mam na myśli biblioteki Slick i Quill). Masz otypowane mechanizmy do tworzenia zapytań do bazy (wprost kontrolujesz jakie to mają być zapytania i jaka ma być ich liczba), ale nie ma żadnych skomplikowanych persistence managerów czy wytrychów typu JPQL.
semicolon
A z czego to wynika? Inaczej, z czego wynika to, że ludzie przestawiają się na prostsze narzędzia. Akurat to mnie ciekawi, bo korpo-świat to jednak takie tłuściejsze rozwiązania preferuje typu springi, angulary. Jak to się stało teraz inaczej piszecie? Ja nie pracuje za często z ORM, bo nie leży mi przyjmowanie wyniku jako klasy, ale trochę rozumiem świat railsów, django, php i to całe bazowanie na frameworkach i ormach.
Wibowit
Nie przypominam sobie by dla Scali był jakiś typowy ORM. Od zawsze używało się jakichś scalowych bibliotek typu właśnie Slick. ORMy są w zasadzie oparte na mutowalności, a Scala ma ambicje łączyć OOP z FP, więc z mutowalnością ma nie po drodze.
semicolon
  • Rejestracja:ponad 5 lat
  • Ostatnio:prawie 5 lat
  • Postów:114
0

@Wibowit jakbyś napisał taką unię w slicku?

Masz tabelkę główną i 3 tabelki podtypy (jako podtyp rozumiem tabelkę, której pk to fk do tabelki głównej). Chcę wykonać na tym union, chcę uzyskać w wyniku kolumny z tabelki głównej, ale też chcę wyprowadzić z pozostałych tabelek dodatkową kolumnę pod wspólnym aliasem. Jak taka rzecz zrobiłbyś w slicku?

Dołączam w załączniku opis tego co rozumiem za podtyp.

W razie potrzeb daj znać to napiszę cały przykład w SQL.

edytowany 4x, ostatnio: semicolon
Wibowit
  • Rejestracja:prawie 20 lat
  • Ostatnio:około 3 godziny
4

@semicolon:

chcę wyprowadzić z pozostałych tabelek dodatkową kolumnę pod wspólnym aliasem

Aliasów na potrzeby zapytania nie ma sensu definiować w Slicku, bo operuje się np na krotkach (i Slick sam tworzy swoje aliasy przy tworzeniu zapytania). Są sobie oficjalne testy https://github.com/slick/slick/blob/6e20f96bba0d84e9bb755b489b0bd73d422ca039/slick-testkit/src/main/scala/com/typesafe/slick/testkit/tests/UnionTest.scala gdzie pokazane jest kilka scenariuszy, np union między dwiema tabelami o różnych kolumnach:

Kopiuj
   class Managers(tag: Tag) extends Table[(Int, String, String)](tag, "managers") {
    def id = column[Int]("id")
    def name = column[String]("name")
    def department = column[String]("department")
    def * = (id, name, department)
  }
  lazy val managers = TableQuery[Managers]

  class Employees(tag: Tag) extends Table[(Int, String, Int)](tag, "employees") {
    def id = column[Int]("id")
    def name = column[String]("name2")
    def manager = column[Int]("manager")
    def * = (id, name, manager)

    // A convenience method for selecting employees by department
    def departmentIs(dept: String) = manager in managers.filter(_.department === dept).map(_.id)
  }
  lazy val employees = TableQuery[Employees]

  def managersQuery = for(m <- managers filter { _.department === "IT" }) yield (m.id, m.name)

  def employeesQuery = for(e <- employees filter { _.departmentIs("IT") }) yield (e.id, e.name)

  val managersData = Seq(
    (1, "Peter", "HR"),
    (2, "Amy", "IT"),
    (3, "Steve", "IT")
  )

  val employeesData = Seq(
    (4, "Jennifer", 1),
    (5, "Tom", 1),
    (6, "Leonard", 2),
    (7, "Ben", 2),
    (8, "Greg", 3)
  )

  def testBasicUnions = {

    val q1 = managersQuery
    val q2 = employeesQuery
    val q3 = (q1 union q2).sortBy(_._2.asc)
    val q4 = managers.map(_.id)
    val q4b = q4 union q4
    val q4c = q4 union q4 union q4
    val q5 = managers.map(m => (m.id, 0)) union employees.map(e => (e.id, e.id))

    (for {
      _ <- (managers.schema ++ employees.schema).create
      _ <- managers ++= managersData
      _ <- employees ++= employeesData
      _ <- mark("q1", q1.result).map(r => r.toSet shouldBe Set((2,"Amy"), (3,"Steve")))
      _ <- mark("q2", q2.result).map(r => r.toSet shouldBe Set((7,"Ben"), (8,"Greg"), (6,"Leonard")))
      _ <- mark("q3", q3.result).map(_ shouldBe List((2,"Amy"), (7,"Ben"), (8,"Greg"), (6,"Leonard"), (3,"Steve")))
      _ <- mark("q4b", q4b.result).map(r => r.toSet shouldBe Set(1, 2, 3))
      _ <- mark("q4c", q4c.result).map(r => r.toSet shouldBe Set(1, 2, 3))
      _ <- mark("q5", q5.result).map(r => r.toSet shouldBe Set((7,7), (6,6), (2,0), (4,4), (3,0), (8,8), (5,5), (1,0)))
    } yield ()) andFinally (managers.schema ++ employees.schema).drop
  }

Jak widać można sobie filtrować, mapować, robić unię i sortować:

Kopiuj
  def managersQuery = for(m <- managers filter { _.department === "IT" }) yield (m.id, m.name)
  def employeesQuery = for(e <- employees filter { _.departmentIs("IT") }) yield (e.id, e.name)
  val q1 = managersQuery
  val q2 = employeesQuery
  val q3 = (q1 union q2).sortBy(_._2.asc)

PS: gdybym miał pisać coś od zera to pewnie wypróbowałbym Quill. Slick miejscami wygląda na przekombinowanego i trochę walczyłem z typami jak chcialem tworzyć abstrakcje (np podmiana JdbcProfiles, abstrakcje na tworzenie, czyszczenie, usuwanie tabel, itd już nawet nie pamiętam, bo dawno nie kombinowałem nic w Slicku).


"Programs must be written for people to read, and only incidentally for machines to execute." - Abelson & Sussman, SICP, preface to the first edition
"Ci, co najbardziej pragną planować życie społeczne, gdyby im na to pozwolić, staliby się w najwyższym stopniu niebezpieczni i nietolerancyjni wobec planów życiowych innych ludzi. Często, tchnącego dobrocią i oddanego jakiejś sprawie idealistę, dzieli od fanatyka tylko mały krok."
Demokracja jest fajna, dopóki wygrywa twoja ulubiona partia.
edytowany 1x, ostatnio: Wibowit
semicolon
Bardzo dziękuję, to jest super przykład. Powinien wisieć w dokumentacji tej biblioteki.
Kliknij, aby dodać treść...

Pomoc 1.18.8

Typografia

Edytor obsługuje składnie Markdown, w której pojedynczy akcent *kursywa* oraz _kursywa_ to pochylenie. Z kolei podwójny akcent **pogrubienie** oraz __pogrubienie__ to pogrubienie. Dodanie znaczników ~~strike~~ to przekreślenie.

Możesz dodać formatowanie komendami , , oraz .

Ponieważ dekoracja podkreślenia jest przeznaczona na linki, markdown nie zawiera specjalnej składni dla podkreślenia. Dlatego by dodać podkreślenie, użyj <u>underline</u>.

Komendy formatujące reagują na skróty klawiszowe: Ctrl+B, Ctrl+I, Ctrl+U oraz Ctrl+S.

Linki

By dodać link w edytorze użyj komendy lub użyj składni [title](link). URL umieszczony w linku lub nawet URL umieszczony bezpośrednio w tekście będzie aktywny i klikalny.

Jeżeli chcesz, możesz samodzielnie dodać link: <a href="link">title</a>.

Wewnętrzne odnośniki

Możesz umieścić odnośnik do wewnętrznej podstrony, używając następującej składni: [[Delphi/Kompendium]] lub [[Delphi/Kompendium|kliknij, aby przejść do kompendium]]. Odnośniki mogą prowadzić do Forum 4programmers.net lub np. do Kompendium.

Wspomnienia użytkowników

By wspomnieć użytkownika forum, wpisz w formularzu znak @. Zobaczysz okienko samouzupełniające nazwy użytkowników. Samouzupełnienie dobierze odpowiedni format wspomnienia, zależnie od tego czy w nazwie użytkownika znajduje się spacja.

Znaczniki HTML

Dozwolone jest używanie niektórych znaczników HTML: <a>, <b>, <i>, <kbd>, <del>, <strong>, <dfn>, <pre>, <blockquote>, <hr/>, <sub>, <sup> oraz <img/>.

Skróty klawiszowe

Dodaj kombinację klawiszy komendą notacji klawiszy lub skrótem klawiszowym Alt+K.

Reprezentuj kombinacje klawiszowe używając taga <kbd>. Oddziel od siebie klawisze znakiem plus, np <kbd>Alt+Tab</kbd>.

Indeks górny oraz dolny

Przykład: wpisując H<sub>2</sub>O i m<sup>2</sup> otrzymasz: H2O i m2.

Składnia Tex

By precyzyjnie wyrazić działanie matematyczne, użyj składni Tex.

<tex>arcctg(x) = argtan(\frac{1}{x}) = arcsin(\frac{1}{\sqrt{1+x^2}})</tex>

Kod źródłowy

Krótkie fragmenty kodu

Wszelkie jednolinijkowe instrukcje języka programowania powinny być zawarte pomiędzy obróconymi apostrofami: `kod instrukcji` lub ``console.log(`string`);``.

Kod wielolinijkowy

Dodaj fragment kodu komendą . Fragmenty kodu zajmujące całą lub więcej linijek powinny być umieszczone w wielolinijkowym fragmencie kodu. Znaczniki ``` lub ~~~ umożliwiają kolorowanie różnych języków programowania. Możemy nadać nazwę języka programowania używając auto-uzupełnienia, kod został pokolorowany używając konkretnych ustawień kolorowania składni:

```javascript
document.write('Hello World');
```

Możesz zaznaczyć również już wklejony kod w edytorze, i użyć komendy  by zamienić go w kod. Użyj kombinacji Ctrl+`, by dodać fragment kodu bez oznaczników języka.

Tabelki

Dodaj przykładową tabelkę używając komendy . Przykładowa tabelka składa się z dwóch kolumn, nagłówka i jednego wiersza.

Wygeneruj tabelkę na podstawie szablonu. Oddziel komórki separatorem ; lub |, a następnie zaznacz szablonu.

nazwisko;dziedzina;odkrycie
Pitagoras;mathematics;Pythagorean Theorem
Albert Einstein;physics;General Relativity
Marie Curie, Pierre Curie;chemistry;Radium, Polonium

Użyj komendy by zamienić zaznaczony szablon na tabelkę Markdown.

Lista uporządkowana i nieuporządkowana

Możliwe jest tworzenie listy numerowanych oraz wypunktowanych. Wystarczy, że pierwszym znakiem linii będzie * lub - dla listy nieuporządkowanej oraz 1. dla listy uporządkowanej.

Użyj komendy by dodać listę uporządkowaną.

1. Lista numerowana
2. Lista numerowana

Użyj komendy by dodać listę nieuporządkowaną.

* Lista wypunktowana
* Lista wypunktowana
** Lista wypunktowana (drugi poziom)

Składnia Markdown

Edytor obsługuje składnię Markdown, która składa się ze znaków specjalnych. Dostępne komendy, jak formatowanie , dodanie tabelki lub fragmentu kodu są w pewnym sensie świadome otaczającej jej składni, i postarają się unikać uszkodzenia jej.

Dla przykładu, używając tylko dostępnych komend, nie możemy dodać formatowania pogrubienia do kodu wielolinijkowego, albo dodać listy do tabelki - mogłoby to doprowadzić do uszkodzenia składni.

W pewnych odosobnionych przypadkach brak nowej linii przed elementami markdown również mógłby uszkodzić składnie, dlatego edytor dodaje brakujące nowe linie. Dla przykładu, dodanie formatowania pochylenia zaraz po tabelce, mogłoby zostać błędne zinterpretowane, więc edytor doda oddzielającą nową linię pomiędzy tabelką, a pochyleniem.

Skróty klawiszowe

Skróty formatujące, kiedy w edytorze znajduje się pojedynczy kursor, wstawiają sformatowany tekst przykładowy. Jeśli w edytorze znajduje się zaznaczenie (słowo, linijka, paragraf), wtedy zaznaczenie zostaje sformatowane.

  • Ctrl+B - dodaj pogrubienie lub pogrub zaznaczenie
  • Ctrl+I - dodaj pochylenie lub pochyl zaznaczenie
  • Ctrl+U - dodaj podkreślenie lub podkreśl zaznaczenie
  • Ctrl+S - dodaj przekreślenie lub przekreśl zaznaczenie

Notacja Klawiszy

  • Alt+K - dodaj notację klawiszy

Fragment kodu bez oznacznika

  • Alt+C - dodaj pusty fragment kodu

Skróty operujące na kodzie i linijkach:

  • Alt+L - zaznaczenie całej linii
  • Alt+, Alt+ - przeniesienie linijki w której znajduje się kursor w górę/dół.
  • Tab/⌘+] - dodaj wcięcie (wcięcie w prawo)
  • Shit+Tab/⌘+[ - usunięcie wcięcia (wycięcie w lewo)

Dodawanie postów:

  • Ctrl+Enter - dodaj post
  • ⌘+Enter - dodaj post (MacOS)