Ulepszony String i sprawa parametrów

Opublikowane przez Jarosław Zabiełło Fri, 29 Jun 2007 00:42:00 GMT

Ruby posiada bardzo bogate możliwości przetwarzania tekstu. Zasugerowany składnią Active Record i wcześniejszym doświadczeniem z Pythonem trochę się zdziwiłem, że Matz nie zaimplementował przekazywania hasza do operatora %.

Parametry w Active Record.

Co prawda Active Record nie miał może nigdy ambicji kompletnego pozbycia się języka SQL, ale na tle takiego SQLAlchemy wypada znacznie mniej obiektowo1. Widać to po sposobie budowania warunków. Active Record umożliwia przekazywanie parametrów do warunków na trzy podstawowe2 sposoby.

“Chamskie” budowanie stringu SQL

Ta metoda jest najbardziej popularna u wszelkiej maści lamerów i osób początkujących. Jest bardzo powszechna wśród pehapowców, którzy niedawno zaczęli bawić się Railsami. Przykładowy kod tego typu wyglądałby mniej więcej tak:

user, passwd = params['user'], params['passwd']
Model.find :all, :conditions => "user='#{user}' AND login='#{passwd}'"

Taka “radosna tfurczość” jest z całą pewnością zaproszeniem do klasycznego ataku SQL Injection. Rails posiada co prawda funkcję sanitize_sql() która pacyfikuje niebezpieczne znaki w kwerendzie SQL, ale można o niej zapomnieć, a kod staje się po prostu brzydki. Zatem tego typu praktyce mówimy stanowcze: fuj!

Prawie wszystkie relacyjne bazy pozwalają na budowanie parametryzowanego SQL’a. Na tym bazują dwie kolejne metody.

Przekazywanie listy.

Warunek :conditions może przyjmować zarówno String jak i Array. W tym drugim wypadku dostajemy za darmo kod uodporniony na atak SQL Injection. Nie trzeba pamiętać o tym czy dane zawierają jakieś niebezpieczne znaki. Nie trzeba też zastanawiać się czy przekazywny parametr jest liczbą, czy stringiem (wymagającym apostrofów w kodzie SQL). Kod jest więc i prostszy i bezpieczniejszy.

user, passwd = params['user'], params['passwd']
Model.find :all, :conditions => ["user=? AND login=?", user, passwd]

Przekazywanie hasza.

Przekazywanie listy ma pewną wadę: trzeba pamiętać o kolejności parametrów. Także zawiera niepotrzebną redundancję w wypadku kiedy jakiś parametr musi się powtarzać. Dlatego Active Record umożliwia korzystanie ze składni inspirowanej bazą Oracle.

user, passwd = params['user'], params['passwd']
Model.find :all, :conditions => ["user=:user AND login=:login", {:user => user, :passwd => passwd}]

lub jeszcze krócej:

Model.find :all, :conditions => ["user=:user AND login=:login",params]

Parametry do stringa.

Po tym wstępie, pora na (niemiłą) niespodziankę jaką spotkałem w implementacji Rubiego. Generalnie Ruby posiada bardzo bogate możliwości przetwarzania tekstu. Podobnie jak Python, posiada też skróconą formę funkcji sprintf realizowaną za pomocą operatora %.

puts "%s can use %s." % ['Ruby', 'list of parameters']
=> "Ruby can use list of parameters."

Python jednakże pozwala także na używanie słowników.

params = {'lang':'Python', 'what':'a dictionary'}
print "%(lang)s can also use %(what)s." % params
Python can also use a dictionary.

Trochę mnie zdziwiło, że Ruby nie posiada odpowiednika tej funkcjonalności…. Na szczęście, dzięki otwartym klasom, monkey patching w przypadku Rubiego nabiera zupełnie innego wymiaru. Nic nie stoi na przeszkodzie aby ulepszyć standardowy obiekt String.

class String  
  alias_method :old_percent, :%
  # Template strings. 
  # Inspired by Python %(key)s and $key syntax.
  def % var
    if var.class == Hash
      var.each_pair do |key, value|
        if key.to_s =~ /^[a-z]+$/
          self.gsub! "$(#{key})", value.to_s
          self.gsub! "$#{key}", value.to_s
        end
      end
    end    
    old_percent var
  end
end

hash = {:lang => 'Ruby', :what => 'a hash'}
puts "Now $lang can also use $what :)" % hash
=> "Now Ruby can also use a hash :)"

I jak tu nie powiedzieć, że Ruby jest piękny? :)


1 Active Record jest też mniej zaawansowany niż np. SQLAlchemy. Funkcjonalność Active Record jest może i świetna, ale jego implementacja w wielu punktach jest z pewnością niedojrzała. Np. nie mam pojęcia dlaczego Active Record zwraca listę obiektów zamiast generator, to pozwoliłoby na większe oszczędności pamięci.

2 Bardziej obiektowe budowanie warunków zapewniają pluginy ez_where i Condition Builder.

Tagi , , ,  | 6 comments

Comments

  1. Avatar hosiawak powiedział about 8 hours later:

    Otwarte klasy Rubiego to najlepsza rzecz od czasu krojonego chleba :) Do innych należą jeszcze np. funkcje jako obiekty (lambda/Proc). Przykład zastosowania ? Wyobraźmy sobie, że budujemy parser HTML’a który za pomocą XPath wyciąga potrzebne nam informacje ze strony WWW, np.

    require 'hpricot'
    def extract(options = {})
      val = @doc.at(options[:xpath])
    end
    
    Sam XPath jednak nie wystarcza, bardzo często będziemy musieli poddać nasze dane obróbce. Jakiej obróbce ? Np. gsub, strip, join itp. Jak zmodyfikować metodę extract aby zapewniła nam taką elastyczność ? Z pomocą przychodzi przekazywanie funkcji jako obiektów, np:
    require 'hpricot'
    
    def extract(options = {})
      val = @doc.at(options[:xpath])
      val = options[:filter].call(val)
    end
    

    Teraz możemy zrobić coś takiego:

    data.extract(:xpath => '/nasz/xpath',
                 :filter => lambda {|x| x.gsub(/,.*$/, '') } )
    
    Potężna koncepcja zapożyczona z Lisp’a która okazuje się bardzo użyteczna w np. w budowaniu elastycznych DSL’i.

    albo powiedzmy:

    data.extract(:xpath => '/nasz/xpath',
                 :filter => lambda {|x| x.split(',')[1].strip } )
    
  2. Avatar Jarosław Zabiełło powiedział about 9 hours later:

    Coś podobnego ma Pyana, gdzie bardzo łatwo można dodawać funkcje Pythona do jej (bardzo szybkiego) parsera XSLT

  3. Avatar lopex powiedział about 15 hours later:

    Jest też bardzo wygodny idiom to robienia templatów (rozszerzyć wedle uznania): s = “some template $a $b” h = { “a” => “A”, “b” => “B” } s.gsub(/(?:\$(\w+))/){h[$1]}

    Btw, xalan nie jest parserem tylko procesorem ;)

  4. Avatar sztywny powiedział about 18 hours later:

    Conditions podawane do ActiveRecord::Base.find mogą być hashem, więc można też pisać po prostu tak:

    user, passwd = params[‘user’], params[‘passwd’] Model.find(:all, :conditions => {:user => user, :passwd => passwd})

  5. Avatar climbus powiedział 4 days later:

    Nie przekonam się do zmiany klasy bez zmiany jej nazwy (dziedziczenie). Przynajmniej wiadomo z czym mam do czynienia.

    Co się stanie gdy ktoś na początku pliku zaimportuje plik zmieniający klasę używaną często poniżej?

  6. Avatar Jarosław Zabiełło powiedział 4 days later:

    climbus: Python też pozwala na modyfikację klas poza tym, że nie da się tego zrobić bezpośrednio na obiektach typu str czy int. Ruby jest tylko bardziej konsekwentny.

(leave url/email »)

   Pomoc języka formatowania Obejrzyj komentarz