Tabele łączyć w javie czy w SQL?

0

Czy stosowanie widoków tabel baz danych w kodzie javowym jest dobrą praktyką?

Jedna tabela:

CREATE dbo.tags as (
    -- ...
    tag varchar(32)
    -- ...
)

druga:

CREATE dbo.countryIsoCodes as (
    -- ...
    isoCode varchar(32),
    -- ...
)

Potrzebuję wszystkie kombinacje tag i isoCode

rozwiązanie z widokiem:

class Service {
    // ...
    List<TagIsoCodeCombination> getAllTagIsoCodeCombinations() {
        return combinationsRepo.findAllCombinations();
    }
}
class CombinationsRepo{
    List<TagIsoCodeCombination> findAllCombinations() {
//...
    String query = "select * from dbo.combinations_view"
//...
   }
}

gdzie dbo.combinations_view to:

SELECT
    j.tag,
    c.isoCode
FROM
    dbo.jobs j crossjoin
    dbo.countries c

rozwiązanie bez widoku:

class Service {
    // ...
    List<TagIsoCodeCombination> getAllTagIsoCodeCombinations() {
        List<TagIsoCodeCombination> result = new ArrayList<>();
        List<String> tags = tagsRepo.findAllTags();
        List<String> countryIsoCodes = countryIsoCodesRepo.findAllCodes();
        for(String tag : tags) {
            for(String countryIsoCode : countryIsoCodes) {
                result.add(new TagIsoCodeCombination(tag, countryIsoCode));
            }
        }
        return result;
    }
}
class TagsRepo {
    List<String> findAllTags() {
//...
    String query = "select tag from dbo.jobs"
//...
    }
}
class CountryIsoCodesRepo {
    List<String> findAllICodes() {
//...
    String query = "select isoCode from dbo.countries"
//...
    }
}
8

Przelewanie z jednego wiadra do drugiego, ja bym zrobił po prostu selecta z joinem, nawet widoku nie potrzeba.

0

Tylko zastanawiam się nad ideą Repository czy to nie powinno odzwierciedlać tabel z bazy...
Robię też repository w wersji plików tekstowych i tu mi głupio zapodawać 2 ścieżki plików...
to może jednak lepiej w serwisie to łączyć, a nie repository...

no chyba, że właśnie widok, wtedy Repository odzwierciedla tabele/widoki z bazy i dorobię se nowy plik z kombinacjami

0

@Julian_: ale repoitory nie zwraca tabel tylko obiekty domenowe xD. Mapowanie tabela per klasa masz w DAO (to co innego!), ale nie zawsze...

0
scibi92 napisał(a):

@Julian_: ale repoitory nie zwraca tabel tylko obiekty domenowe xD. Mapowanie tabela per klasa masz w DAO (to co innego!), ale nie zawsze...

Może mały offtop, ale czy dobrze Cię rozumiem @scibi92 że jeśli mam w klasie select * from tabela1 to jest to DAO, a jak mam select * from tabela1, tabela2 where id1=id2 to jest to repozytorium? Bo ja różnicy nigdy nie widziałem, i stosowałem tych terminów zamiennie

Koniec offtopu

@Julian_ (zakładając że DAO i repozytorium to terminy różne) to używa się albo dao albo repozytorium jeśli dao nie łączy się bezpośrednio do źródła danych to nie jest to dao, i podobnie repozytorium, jeśli nie łączy się bezpośrednio do źródła danych to nie jest to repozytorium, tylko kolejny serwis

@Julian_ link który podałeś (https://stackoverflow.com/a/11384997/13492634 ) utwierdza mnie w przekonaniu że DAO i Repozytorium z technicznego punktu widzenia to jest to samo. I wiem że w tej myśli nie jestem sam na forum

7

Jestem wielkim fanem clean codu i DDD, natomiast zastanawiam się, ile czasu można przepalić na teoretyzowaniu zamiast zrobić selecta z joinem.

4
KamilAdam napisał(a):

@Julian_ link który podałeś (https://stackoverflow.com/a/11384997/13492634 ) utwierdza mnie w przekonaniu że DAO i Repozytorium z technicznego punktu widzenia to jest to samo. I wiem że w tej myśli nie jestem sam na forum

No jak możesz nie widzieć różnicy? Przecież to by oznaczało, że nie nadajesz się na swoje stanowisko, albo jeszcze gorzej....

Oczywiste jest, że DAO, po zakomunikowaniu, że robimy DDD staje się Repozytorium - momentaaaalnie. Czego tu można nie rozumieć? (No dobra - trzeba jeszcze suffix klasy zmienić).

0

OK, to zrobiłem te repozytoria bardziej domenowo. Czy taki kod można wybaczyć?

class RequestFileRepository() extends Repository[Request] {

  val maxAge = MaxAge.Week
  val countriesFilePath = "src/main/resources/in/countries.csv"
  val tagsFilePath = "src/main/resources/in/tags.csv"
  val tagTranslationsFilePath = "src/main/resources/in/tag_translations.csv"
  val providerHostsFilePath = "src/main/resources/in/provider_hosts.csv"

  val fileIOEngine: FileIOEngine = new FileIOEngine

  override def find: Seq[Request] = {
    val countries = fileIOEngine.readCsvLines(countriesFilePath)
    val tags = fileIOEngine.readCsvLines(tagsFilePath)
    val tagTranslations = fileIOEngine.readCsvLines(tagTranslationsFilePath)
    val providerHosts = fileIOEngine.readCsvLines(providerHostsFilePath)

    val tagTranslationMap = tagTranslations.map(t => (t(0), t(1)) -> t(2)).toMap
    val providerHostMap = providerHosts.map(p => (p(0), p(1)) -> p(2)).toMap

    val tagTagIdAndIsoCode = tags.flatMap(t => countries.map(m => (t(1), t(0), m(0))))
    val tagTranslatedAndTagIdAndIsoCode = tagTagIdAndIsoCode.map(t => (t._1, tagTranslationMap.getOrElse((t._2, t._3), t._1), t._2))
    val providerIdAndTagAndTagIdAndIsoCode = providerHosts.flatMap(p => tagTranslatedAndTagIdAndIsoCode.map(t => (p(0), t._1, t._2, t._3)))
    val hostAndProviderIdAndTagAndTagIdAndIsoCode = providerIdAndTagAndTagIdAndIsoCode.map(p => (providerHostMap.get((p._1, p._4)).get,p._1, p._2, p._3, p._4))


    hostAndProviderIdAndTagAndTagIdAndIsoCode.map(h => {
      val host = h._1
      val providerId = h._2.toInt
      val tag = h._3
      val tagId = h._4.toInt
      val isoCode = h._5
      Request(providerId, tagId, tag, isoCode, host, maxAge)
    })
  }

}

head plików:

countries.csv
---
AR,argentina
AU,australia
AT,austria
BH,bahrain
BE,belgium
BR,brasil
...
provider_hosts.csv
---
1,AR,https://ar.indeed.com/
1,AU,https://au.indeed.com/
1,AT,https://at.indeed.com/
1,BH,https://bh.indeed.com/
1,BE,https://be.indeed.com/
1,BR,https://www.indeed.com.br/
tag_translations.csv
---
1,PL,hadupek
2,PL,habejsowiec
3,PL,hif
4,PL,sparkownik
5,PL,klołdowiarz
tags.csv
---
1,hadoop
2,hbase
3,hive
4,spark
5,cloud

Czy mogę nie dorabiać do tych przekształceń klas? A jeśli powinienem to czy mogą być prywatne w klasie RequestFileRepository?
Niestety scala nie ma dataframów, a do tego chyba nie ma sensu zaprzęgnąć sparka tylko po to, żeby te joiny robić na dataframach albo sparkowym SQL?

1

Cały wątek rozmawialiśmy o sqlu, a tu są jakieś csvki WTF?

0
Charles_Ray napisał(a):

Cały wątek rozmawialiśmy o sqlu, a tu są jakieś csvki WTF?

wczoraj 22:37

Julian_ napisał(a):

Robię też repository w wersji plików tekstowych i tu mi głupio zapodawać 2 ścieżki plików...

Mam repozytoriom w bazie i jego replikę w plikach. Wszystko co zapisuje do bazy zapisuje też do plików o takiej samej strykturze, żeby wrazie czego można było importować pliki do bazy

0

Lepiej już nie umiem:

class RequestService(
                      tagsDao: TagsDao,
                      tagTranslationsDao: TagTranslationsDao,
                      countriesDao: CountriesDao,
                      providersDao: ProvidersDao,
                      providerHostsDao: ProviderHostsDao,
                      maxAge: MaxAge
                    ) extends Service[Request] {


  override def find: Seq[Request] = {

    case class Combination1(tagId: Int, tag: String, countryIsoCode: String)
    case class Combination2(tagId: Int, tag: String, countryIsoCode: String, providerId: Int, httpSchema: String)
    case class Combination3(tagId: Int, tag: String, countryIsoCode: String, providerId: Int, httpSchema: String, httpHost: String)

    val tagsDto = tagsDao.find.get
    val tagTranslationsDto = tagTranslationsDao.find.get
    val countriesDto = countriesDao.find.get
    val providersDto = providersDao.find.get
    val providerHostsDto = providerHostsDao.find.get

    val tagTranslationMap = tagTranslationsDto.map(t => (t.tagId, t.countryIsoCode) -> t.tag).toMap
    val providerHostMap = providerHostsDto.map(p => (p.providerId, p.countryIsoCode) -> p.httpHost).toMap

    val translatedTagCountryCombinations =
      tagsDto
        .flatMap(t => countriesDto.map(m => Combination1(t.id, t.tag, m.isoCode)))
        .map(t => {
          val key = (t.tagId, t.countryIsoCode)
          val translatedTag = tagTranslationMap.getOrElse(key, t.tag)
          Combination1(t.tagId, translatedTag, t.countryIsoCode)
        })

    val rows =
      providersDto
        .flatMap(p => {
          translatedTagCountryCombinations.map(t => new Combination2(t.tagId, t.tag, t.countryIsoCode, p.id, p.httpSchema))
        })
        .map(t => {
          val key = (t.providerId, t.countryIsoCode)
          val httpHost = providerHostMap.get(key).get
          Combination3(t.tagId, t.tag, t.countryIsoCode, t.providerId, t.httpSchema, httpHost)
        })

    rows.map(r => Request(
      r.tagId,
      r.tag,
      r.countryIsoCode,
      r.providerId,
      r.httpSchema,
      r.httpHost,
      maxAge
    ))
  }

}
4
Julian_ napisał(a):
Charles_Ray napisał(a):

Cały wątek rozmawialiśmy o sqlu, a tu są jakieś csvki WTF?

wczoraj 22:37

Julian_ napisał(a):

Robię też repository w wersji plików tekstowych i tu mi głupio zapodawać 2 ścieżki plików...

Mam repozytoriom w bazie i jego replikę w plikach. Wszystko co zapisuje do bazy zapisuje też do plików o takiej samej strykturze, żeby wrazie czego można było importować pliki do bazy

Ja to widzę tak:

0

O, w scali można jak w R robić funkcje zagnieżdżone, no to myk:

class RequestService(
                      tagsDao: TagsDao,
                      tagTranslationsDao: TagTranslationsDao,
                      countriesDao: CountriesDao,
                      providersDao: ProvidersDao,
                      providerHostsDao: ProviderHostsDao,
                      maxAge: MaxAge
                    ) extends Service[Request] {

  val logger: Logger = Logger(LoggerFactory.getLogger(this.getClass))

  override def findAll: Seq[Request] = {
    def validRequest(r: Request) = {
      if (!r.providerHost.host.country.equals(r.tag.country)) {
        val msg = "Country of tag is " + r.tag.country + " and country of host is " + r.providerHost.host.country
        logger.error(msg)
        throw new InvalidStateException(msg)
      }
    }

    def getRequest(t: TagsDto, p: ProviderHostsDto, countryMap: Map[String, String], tagTranslationMap: Map[(Int, String), String], providerHostMap: Map[Int, String]): Request = {
      def getCountry: Country = {
        Country(p.countryIsoCode, countryMap(p.countryIsoCode))
      }

      def getTag(country: Country): Tag = {
        val tagKey = (t.id, p.countryIsoCode)
        val translatedTag = tagTranslationMap.getOrElse(tagKey, t.tag)
        Tag(t.id, translatedTag, country)
      }

      def getProviderHost(country: Country): ProviderHost = {
        val host = Host(country, p.httpHost)
        val httpSchema = providerHostMap(p.providerId)
        ProviderHost(p.providerId, httpSchema, host)
      }

      val country = getCountry
      val tag = getTag(country)
      val providerHost = getProviderHost(country)

      Request(tag, providerHost, maxAge)
    }

    val tagsDto = tagsDao.find.get
    val tagTranslationsDto = tagTranslationsDao.find.get
    val countriesDto = countriesDao.find.get
    val providersDto = providersDao.find.get
    val providerHostsDto = providerHostsDao.find.get

    val countryMap = countriesDto.map(c => c.isoCode -> c.name).toMap
    val tagTranslationMap = tagTranslationsDto.map(t => (t.tagId, t.countryIsoCode) -> t.tag).toMap
    val providerSchemaMap = providersDto.map(p => p.id -> p.httpSchema).toMap

    val requests = tagsDto.flatMap(t => providerHostsDto.map(p => {
      getRequest(t, p, countryMap, tagTranslationMap, providerSchemaMap)
    }))

    requests.foreach(validRequest)

    requests
  }

  override def save(seq: Seq[Request]): Unit = ???

}
3

Według mnie warto wspomnieć o kilku aspektach

  1. Physical Joins na poziomie bazy danych.
  • Serwery bazodanowe robią sporo optymalizacji do joinow. Instnieja różne algorytmy które są używane w zależności od sytuacji(SQL server ma dobra dokumentacje nt Physical joins). W momencie w którym decydujesz że chcesz robić joiny sam, tak naprawdę rezygnujesz z tej funkcjonalności.
  1. Cache
  • Bazy danych cachuja sporo danych, włączając w to całe zapytania. Jeżeli robisz joiny w Javie, to kolejny raz rezygnujesz z ważnej funkcjonalności serwera bazy danych. Po prostu możesz powtarzać tą samą operację dla dokładnie tych samych danych.
  1. Przepustowość dysku
  • Baza danych może przeczytać dane albo z dysku albo z cachea. Zakładając, że robisz joiny w bazie danych oraz całe zapytanie jest czytane z cache-a to wtedy oszczędzasz na przepustowości. Oczywiście, oddzielne zapytania które będziesz łączył w Javie też mogą być czytane z cache-a, jednak zazwyczaj będzie to więcej danych, a więc serwer bazodanowy mając ograniczona przestrzeń dla cache-a może się ich pozbyć wcześniej aniżeli całego zapytania, może też być odwrotnie jezeli poszczególne zapytania byłyby używane częściej aniżeli join. To zalezy.
  1. Ilość danych wysyłanych przez sieć
  • Jeżeli zrobisz join-a w bazie danych to zazwyczaj tych danych wysyłanych przez sieć będzie mniej.
  1. Zasoby w Twoim serwisie
  • Jeżeli będziesz robił join-a sam to całkiem prawdopodobne że pamięci i CPU będziesz używaj więcej.

Ogólnie rzecz biorąc to takie problemy możesz spotkać w mikrouslugach, jeżeli używasz wzorca Database Per Service. Większość z tych punktów zależy od ruchu na serwerze, ilości danych itd. Ogólnie to ciekawy temat ale warto na każdy przypadek patrzeć oddzielnie.

1

Tu trzeba dodać, że Cache działa również w javie itp. i wtedy jest jeszcze lepiej, bo nie dość, że dane mamy błyskawicznie to oszczędzamy na konwersji,transferze IO, dysku itp.
Nie do pobicia.
Oczywiście wszystko zależy od scenariusza. Dane typu kraje (nazwy) to malutkie zbiory i angażowanie jakiejkowiek bazy w to jest dla mnie dziwne - szczególnie, że jakiś specjalnch raportów się z tego nie zrobi...

0

Załóżmy, że mam w bazie 3 tabele:countries, provider i tag. Łącząc je, składam z nich Request.
Jak w Repository połącze te tabele na bazie joinem to Repository zwraca RequestDto czy od razu Request?

No bo te tabele na bazie będę też zapisywał to i tak muszę dorobić CountriesDto, ProviderDto i TagDto.

0

Pachnie kultem cargo. A co to za różnica? Ja bym wziął bez „Dto” - krótsza nazwa.

0
Charles_Ray napisał(a):

Pachnie kultem cargo. A co to za różnica? Ja bym wziął bez „Dto” - krótsza nazwa.

Chdozi mi o to czy mają być 2 klasy dla Request.

Repository ---RequestDto---> Service ---RequestDomain--->

czy

Repository ---RequestDomain---> Service ---RequestDomain--->

RequestDomain obiektowy, a RequestDto płaski.

case class RequestDto(
                      tagId: Int,
                      tagName: String,
                      tagCountryIsoCode: String,
                      providerId: Int,
                      providerHttpSchema: String,
                      providerHttpHost: String
                      maxAgeDays: Int
                    )

case class RequestDomain(
                         tag: Tag,
                         providerHost: ProviderHost,
                         maxAge: MaxAge
                        )

1 użytkowników online, w tym zalogowanych: 0, gości: 1