Languages

Erudis - your road to knowledge
Scala w akcji. Baza danych w 99 wierszach kodu
Streszczenie: 

 

Mamy wreszcie potrzebną wiedzę, żeby zająć się utworzeniem bazy danych, która będzie zapisywała i odczytywała dane reprezentowane przez obiekty RSSFeedBean (Listing 3). Jak zwykle zaczniemy od przyjrzenia się kodowi źródłowemu, którego najciekawsze fragmenty znajdują się na Listingu 5. Dane są przechowywane w pliku CSV, reprezentowanym przez obiekt dbFile, a w pamięci są przechowywane przy pomocy kontenera HashMap. W konstruktorze sprawdzamy, czy dostaliśmy poprawny obiekt File (zawartość konstruktora to wszystko to, co jest umieszczone po deklaracji klasy), jeżeli nie, to wyrzucamy wyjątek.

Listing 5. Implementacje prostej bazy danych opartej o plik CSV

package pl.erudis.feedseater.db
import java.io._
import io.Source
import collection.mutable.HashMap
import java.net.URL
import java.util.regex.Pattern
import scala.xml._
/**
 * Baza danych w pliku CSV przechowująca obiekty RSSFeedBean
 */
class CSVDb(dbFile : File) {
   //konstruktor
   if (dbFile == null) throw new IllegalArgumentException("No database file")
   //pojemnik na dane
   var feeds = new HashMap[String,RSSFeedBean]
   if(!dbFile.exists){
      dbFile.createNewFile
   }
   //czytamy dane z pliku
   read
   /**
    * Dodaje rekord do bazy
    * @param feed informacja do zachowania
    */  
   def add(feed: RSSFeedBean) : Unit = {
      add(List(feed))
   }
   /**
    * Dodaje do bazy listę rekordów
    * @param rssf informacja do zachowania
    */  
   def add(feedList : List[RSSFeedBean]) : Unit = {
      feedList.foreach(feed => feeds(feed.getId) = feed)
      save(true)
   }
   
   /**
    * Wyciąga pojedynczy rekord z bazy danych
    * @param identyfikator rekordu
    * @return bean z informajcą
    */
   def getBean(recordId: String): RSSFeedBean = feeds(recordId)
   /**
    * Wyciąga pojedynczy rekord z bazy
    * @param identyfikator rekordu
    * @return tupla Tuple3[String, URL, String] z danymi
    */
   def get(recordId: String) = (feeds(recordId).title, 
         feeds(recordId).url, feeds(recordId).description)
   def update(recordId: String, feed: RSSFeedBean){
      feeds(recordId) = feed
      save(false) //zawartość pliku musi być nadpisana
   }
   def delete(recordId: String){
      feeds - recordId
      save(false) //zawartość pliku musi być nadpisana
   }
   /**
    * Wyszukuje podanego łańcucha w title i description
    */
   def search(needle: String) : List[RSSFeedBean] = {
      def check(feed: RSSFeedBean): Boolean = Pattern.matches(needle, feed.title)
            || Pattern.matches(needle, feed.description)
      return feeds.values.filter(f => check(f) == true).toList
   }
   /**
    * Zwraca zawartość bazy danych w postaci XML-a
    */
   def getXML(): Elem = { 
      <wpisy>
      {
	 for (feed <- feeds.values)  
	   yield <wpis id={feed.getId}>
	            <tytul>{feed.title}</tytul>
	            <opis>{feed.description}</opis>
	            <link>{feed.url}</link>
	         </wpis>     
      }
      </wpisy>
   }
   /**
    * Wczytuje dane z pliku do mapy feeds
    */
   private def read = {
      def createFeed(str: String) = {
         val arr = str.split(",")
         if(arr.length == 4){ //sprawdzenie na wypadek pustego pliku
            val rssf = new RSSFeedBean(arr(1), new URL(arr(2)), arr(3))
            rssf.setId(arr(0))
            feeds(arr(0)) = rssf
         }
      }
      Source.fromFile(dbFile).getLines.foreach(line => createFeed(line))
   }
   /**
    * Zapisuje zawartość mapy feeds w pliku  
    */  
   private def save(append: Boolean) : Unit = {
      var printer: PrintWriter = null
      //zapisuje pojedynczy wiersz
      def saveSingle(feed: RSSFeedBean) = {
         printer.print(feed.getId); printer.print(",");  
         printer.print(feed.title); printer.print(",");  
         printer.print(feed.url); printer.print(",");  
         printer.println(feed.description);
      }
      try{ 
         printer = new PrintWriter(new BufferedWriter(new FileWriter(dbFile, append)))
         feeds.values.foreach(feed => saveSingle(feed))
      }catch{
         case ioEx: IOException => {ioEx.printStackTrace}
         case fnfEx: FileNotFoundException => {fnfEx.printStackTrace}
         case _ => Console.println("Inny błąd...") 
      }finally{
         printer.close
      }
   }      
}

Tym razem, dla odmiany, rozpoczniemy dokładną analizę kodu od dołu, od dwóch prywatnych metod read i save. Metoda read wczytuje do mapy feeds dane wykorzystując metodę Source.fromFile. Scala pozwala odczytać dane z pliku w naprawdę prosty sposób. Oczywiście jeśli ktoś woli się pomęczyć, to może skorzystać z API wejścia-wyjścia dostępnego w Javie, którego z kolei musimy użyć do pisania do pliku (metoda save).

Metoda save jest napisana porządnie gdyż, w odróżnieniu od metody read, obsługuje sytuacje wyjątkowe. Widzimy więc, że obsługa wyjątków nie jest wymuszona (tak jak w Javie), dodatkowo implementujemy ją w bardzo oryginalny, ale wygodny sposób.

Scala jest wyposażona w mechanizm dopasowywania wzorców do klas (ang. pattern matching). Jest on uogólnieniem używanej w wielu językach programowania konstrukcji switch. Klasyczny switch przyjmuje jako argument liczbę, która jest następnie porównana z serią warunków (case), gdy któreś sprawdzenie wypadnie pozytywnie, to wykonywana jest zawartość klauzuli case. W Scali działanie jest analogiczne, tyle że elementami, które mogą być sprawdzane nie są tylko wartości liczbowe, ale całe klasy, także utworzone przez nas (muszą być to specjalne klasy, zdefiniowane jako case class, o czym powiemy sobie jeszcze parę słów).

W metodzie save wyrzucenie wyjątku wewnątrz bloku try przekazuje sterowanie do bloku catch, gdzie przy pomocy case sprawdzamy jaki konkretnie wyjątek został wyrzucony i odpowiednio na niego reagujemy. Jeżeli nie trafimy w żaden wyjątek pozostaje klauzula case przyjmująca dowolny obiekt, reprezentowany przez znak podkreślenia, pełniący w Scali rolę symbolu wieloznacznego.

Wszystkie metody modyfikujące zawartość bazy danych najpierw wprowadzają odpowiednie zmiany w pamięci, w mapie feeds, a następnie przy pomocy save synchronizują zawartość mapy z plikiem CSV. Operacje na mapie są bardzo proste dzięki przeciążonym operatorom + i -.

Spośród metod wyciągających dane ciekawa jest metoda get, która zwraca rekord z bazy danych jako tuplę o trzech elementach. W pewnych sytuacjach znacznie wygodniejsze jest posługiwanie się tuplą zamiast obiektem JavaBean.

Zdecydowanie najbardziej użyteczną metodą jest search(String), wyszukująca rekordów według podanego wyrażenia regularnego. W jaki sposób jest ona skonstruowana? Zaczniemy analizę tej metody od dołu: na mapie feeds wywołujemy metodę values, zwracającą listę obiektów RSSFeedBean. Następnie wywołujemy metodę filter, której zadaniem jest wybranie tych elementów, które pasują do podanego wzorca. Zajmuje się tym metoda check, wykorzystująca obsługę wyrażeń regularnych wziętą z Javy.

Właśnie tutaj w pełnej okazałości swoją siłę pokazuje Scala. Mamy do dyspozycji potężne narzędzia do wykonywania operacji na kontenerach, a jednocześnie, jeśli nie znamy dobrze wszystkich niuansów Scali, w każdej chwili możemy posłużyć się językiem Java. Implementując funkcję check nie zastanawiałem się specjalnie, czy Scala ma w ogóle wbudowaną obsługę wyrażeń regularnych. W przypadku innego, nie znanego mi dobrze języka moje lenistwo lub ignorancja byłyby natychmiast ukarane i musiałbym tak czy inaczej poświęcić czas na studiowanie obsługi wyrażeń regularnych w danym języku programowania. Jeśli czegoś nie umiem zrobić w Scali, to nie przejmuję się tym, robię to tak, jak w Javie i koniec.

Nie można też nie wspomnieć o ważnej cesze Scali, która jest wykorzystywana w wielu miejscach klasy CSVDb, czyli o możliwości zdefiniowania funkcji wewnątrz funkcji. Jest to bardzo wygodne i pozwala na podział dużych metod na małe, logicznie spójne fragmenty.

Pominęliśmy w naszej analizie metodę getXML, która wygląda dość niezwykle. Metoda ta zwraca zawartość bazy danych w postaci dokumentu XML, żeby zrozumieć jej działanie musimy zająć się dokładniej obsługą XML-a w Scali. Pozwoli nam to uzupełnić naszą aplikację o najważniejszą funkcjonalność, czyli pobieranie informacji z kanałów RSS.