Ulepszony String i sprawa parametrów
Posted by 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.


Kanały IRC![[Dilber w Onecie]](/images/larry.png)


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]) endSam 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) endTeraz 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 } )Coś podobnego ma Pyana, gdzie bardzo łatwo można dodawać funkcje Pythona do jej (bardzo szybkiego) parsera XSLT
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 ;)
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})
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?
climbus: Python też pozwala na modyfikację klas poza tym, że nie da się tego zrobić bezpośrednio na obiektach typu
strczyint. Ruby jest tylko bardziej konsekwentny.