среда, апреля 30, 2008

On Prototype vs jQuery


I see more and more posts like this or this recently and I always see that when people are telling about how poor the Prototype framework is they really don't know even half of it's possibilities.

As it always happens, the code comparison in those posts is written by person who is not experienced in one framework or another.

I agree, that jQuery has some nice features: 1) actions on groups of objects (that you have mentioned), 2) more powerful selectors: prototype has support for css3 selectors only, but jQuery can handle more sophisticated queries like selecting hidden elements or inputs of particular type.

I haven't dug into what it takes to write custom effect or plugin (plan to do it soon), but I don't see how you can base your effects on existing ones in a way other than aggregating/compositing (the thing with Prototype effects is that you can inherit your effect from some base effect and have some nice features like configurable 'easing' function, which can control effect speed over time, or some other stuff for free).

But seeing syntax comparisons like that just makes me mad.
First, adding a class name to element in Prototype is just as easy as in jQuery:

$('element').addClassName('className')
$('element').removeClassName('classsName')

Second, the Ajax syntax. Prototype's variant is designed to do POST requests by default. You don't need to specify 'method' parameter if you do POST request. As for GET requests, in Prototype you can do just as in jQuery:

new Ajax.Updater('element', url + '&' + parameters)

(I believe, the '&' thing is needed in jQuery also, but author of code comparison just 'forgot' to mention that)

Moreover, if you don't like the wordy syntax of Ajax.Updater, you can extend Element to support whatever syntax you like:

Element.addMethods({
load: function(element, url) {
new Ajax.Updater(element, url)
}
})

and then you can do

$('element').load(url + params)

just as in jQuery.

And, yes, Prototype doesn't handle working with sets of elements by default. To add a particular class to a set of elements in Prototype you need to do some extra stuff, but not as it is mentioned in code comparison:

$$('.element').invoke('addClassName', 'className')

And yes, I agree, that Prototype and Scriptaculous have bad documentation and I personally have bought a book on them (see Pragmatic Programmers bookshelf) and my problems with Prototype and Scripaculous have gone since then.

I don't see any big reason to switch to jQuery.


PS There are lots of other javascript frameworks like mooTools (if I spell it correctly), Dojo, Rhino and such and I always haven't paid much attention to those, because usually my web application require custom solutions, not those that are present in such frameworks. But recently I came across some book ("Mastering Dojo" published by Pragmatic Programmers) and a podcast on it and it got my attention. They say that lot's of fortune 500 companies are using their framework because it is very solid and stable. And I'm curious about what particular features of Dojo make them think so. I have visited Dojo's site and tried to see at some of the examples (e.g. fish eye component) and they were running way too slow on my latest Firefox 2.x (which I consider as rather modern web browser). Probably, I need to give it one more chance and, probably, read that book.

вторник, декабря 11, 2007

ActiveRecord's database shortcut methods

I see more and more people nowadays complain about other people using destroy_all to delete a bunch of records without understanding the performance decrease compared to using delete_all.

Well, I'll tell you what: in general they do the right thing. My opinion is that having such shortcut methods like delete_all (or update_attribute that issues an update command without triggering validation/callbacks) are evil. While ActiveRecord tries to provide a foundation for building your domain model (smart model, not just containers for data) those methods allow to mess everything up. E.g. you have a model, and when you decide to provide a means to delete all objects, you use delete_all because of better performance than destroy_all. Later on you decide to add some on_destroy callbacks, and then you find out that you need to change every call to delete_all to destroy_all.

Calling delete_all just breaks an invariant for your objects.

What it would be nice is to optimize destroy_all to just call delete_all if nothing else is required. The main reason to call destroy_all is the on_destroy callback. So, ActiveRecord could check if there is any callbacks and if there is no callbacks, then just call delete_all operation.

The same could be done for updates (and it was discussed for a long time): update only those fields that have changed. Then, instead of calling "special" method to update just one field, you will just update the required field and call regular #save. AR will detect all changed fields and issue an update command for just those fields.

How this can be implemented ? The ideal solution is to keep a set of "unmodified" data and compare object's data to that "unmodified" set on save. But on big objects it could require two times more memory. Instead, it could be easier to track what fields were assigned values to and set "changed" flag for that field. If the flag wasn't yet set and the assigned value differs from the one that it is now, then set the flag. If the flag was already set, then don't update it. Of course, it will sometimes update the field to the same value if you first set it to some other value and then set the value that was before. But I believe that such cases are rare and it is not a problem if we update it with the same value.
Storing a boolean flag per database column is not an issue also.

вторник, ноября 06, 2007

Giles Bowkett: IRB: What Was That Method Again?

Giles Bowkett: IRB: What Was That Method Again?
why should someone bother adding just another method to do stuff that is easily done with existing methods ?
"any arbitrary string".methods.grep("ch")

On Auto Migrations

There is an auto_migrations plugin that people nowadays start using more and more often. This plugin allows to maintain only one file with database schema definitions and it changes DB structure to match that definition automagically.

Giles Bowkett blogged about it recently and about problems with regular migrations. But wait a minute, I've seen (and experienced) only one type of situations when you can't migrate from zero up to latest migration: it's situation when your model has changed through version and e.g. some method vital for that migration was removed or some new validation was added and you try to create objects in migration that have that field set to an invalid value.

How can changing DB structure make migrations invalid ?

And having that in mind, how that auto_migrations thing can help ? Also, how will you actually migrate your data with auto_migrations ?

PS btw, the way to fix broken migrations in situation described above is to use schema dump that rails creates anyways in db/ directory: when you create from scratch, there could be no data to migrate, so you could just create the DB structure. But if there are migrations to e.g. create an admin user, then you could need to run actual migrations. And the only way I see (when they fail half way) is to use version control system to update to each revision where new migration was added (so that source code match the migration), do the migration, then update to next version where new migration was introduced, do the migration and so forth.
Probably, this could be automated. Assuming it is needed only when new member joins the development team, it is a rear operation so the performance is not critical.

четверг, июля 05, 2007

Завершение саги о датах


Никак не мог успокоиться: все решения, которые я находил, не содержали решения для выдачи ошибок, если дату невозможно распарсить. Чаще всего, если попытка перевода строки в дату не была успешной, вместо даты - nil и никаких тебе ошибок.

Наконец мне удалось сложить все кусочки воедино и получить достаточно неплохое, на мой взгляд, решение.

1. Как редактировать.
Как уже обсуждалось в моих предыдущих постах, самое правильное при редактировании даты - отказаться от трех списков и оставить только одно текстовое поле, куда можно вводить строковое представление даты в определенном формате. Плюс можно прикрутить жаваскриптовый календарик к этому текстовому полю.
Как же высветить введенное значение, если распарсить строку не удалось ? Вспоминаем, что для этого есть *_before_type_cast атрибуты. Напомню: когда мы присваиваем значения конкретному атрибуту, это значение сохраняется внутри экземпляра модели нетронутым (про многопараметровые значения мы не говорим). Конвертация же происходит, когда мы пытаемся получить значение атрибута экзепляра класса модели, а *_before_type_cast возвращает значение, обходя конвертацию. Поэтому, надо только сделать date_select хэлпер, который будет пытаться сконвертировать значение атрибута (сконвертированное, т.е. типа Date) в строку, если это значение не nil, иначе - выкладывать строковое представление значение *_before_type_cast. Плюс код для яваскриптового календарика (например, этого).
Отлично, теперь можно вводить даты в стандартных форматах (например, YYYY-MM-DD) и не терять введенное значение, если преобразовать его в дату не удастся.

2. Нужные форматы даты.
В моем приложении просто необходимо, чтобы дату можно было вводить в русском формате (т.е. DD.MM.YYYY). Поэтому, надо как-то научить дату понимать такой формат.
Поизучав исходники, я пришел к тому, что конвертация из строки в дату производится экземпляром объекта, который представляет соответствующий столбец базы данных. По реализации становится ясно, что конвертация осуществляется методом ParseDate#parsedate стандартной библиотеки Ruby. Недолго думая, расширяем этот метод, чтобы он понимал нужный нам формат даты:
ParseDate.class_eval do
class << self
def parsedate_with_ru_format(str, comp=false)
str = str.to_s
str = "#{$3}-#{$2}-#{$1}" if /(
\d{2})\.(\d{2})\.(\d{4})/ =~ str
parsedate_without_ru_format(str, comp)
end

alias_method_chain :parsedate, :ru_format
end
end

Подключаем этот код в config/environment.rb и вуаля: Rails понимает нужный нам формат даты.

3. Ошибка при неправильном формате даты.
Теперь формат понимаем, неправильное значение никуда не девается, осталось только понять, что формат даты неверный. В текущем состоянии при ошибке конвертации вместо даты имеем nil. Это может быть немного confusing для пользователя, если он думал, что он заполнил (необязательное) поле с датой и сохранил его, а потом выяснит, что в базе вместо значения получился nil.
При этом, не хотелось бы добавлять никаких явных валидаций (типа, validates_date_format :foo и т.п.). Итак: прикручиваем к ActiveRecord::Base валидацию по умолчанию, которая перебирает все столбцы модели и для всех столбцов типа :date проверяет, что если сконвертированное значение == nil, а значение *_before_type_cast не пустое (!foo_before_type_cast.empty?), то добавляет ошибку на это поле.
ActiveRecord::Base.class_eval do
class << self
def date_columns
columns.select { |column| column.type == :date }
end
end

def validate_dates_format(record)
record.class.date_columns.each do |column|
if record.send(column.name).nil? &&
record.send("#{column.name}_before_type_cast").empty?
record.errors.add(column.name, "has invalid date format")
end
end
end

validate :validate_dates_format
end

Данное решение избавляет еще и от проблем с невозможностью легко выставить дату для атрибута, который защищен от массового присвоения (посредством, например, attr_protected).

Надеюсь, описанный выше способ пригодится кому-нибудь (лично меня он полностью устраивает) или послужит источником знаний/вдохновения для написания "следующего самого клевого плагина для Rails".

PS Конечно, расширение стандартной библиотеки для поддержки своих форматов строк не самое изящное регение и черевато проблемами, если вдруг разработчики решат использовать какой-либо другой метод конвертации строк в даты. Но я считаю, что на данный момент этого решения вполне достаточно.