Thursday, April 2, 2009

Single Table Inheritance in Ruby on Rails

Недавно, при попытке портирования одного приложения с Java на RoR столкнулся с проблемой "отображения иерархии классов на реляционную модель". Известное дело в Java или .NET я бы использовал Hibernate/NHibernate и Single Table Inheritance или STI (в данном случае это наиболее подходящий из 3 представленных способов). На мое счастье, оказывается ActiveRecord в RoR, также поддерживает STI. Итак, приступим.

Предметная область


В качестве предметной области я взял модель безопасности приложения (т.е. ее часть). У нас есть базовая сущность Principal(name, description), и три конкретные сущности наследника: User(login, passwordHash, lastLoginDate, expirationDate, accountDisabled), Role, Group. Для данного примера я решил не усложнять модель связями между User, Role и Group.

Реализация


Для реализации STI нам потребуется в таблицу principals добавить зарезервированное поле type и все поля из классов: Principal, User, Role и Group.

Скрипт миграции


Привожу скрипт миграции для таблицы principals.

class CreatePrincipals < ActiveRecord::Migration
def self.up
create_table :principals do |t|
t.column :name, :string, :limit => 64
t.column :description, :string
t.column :account_disabled, :boolean
t.column :last_login_date, :date
t.column :expiration_date, :date
t.column :type, :string, :limit => 48
end
end

def self.down
drop_table :principals
end
end


Модель


Сущность Principal

class Principal < ActiveRecord::Base
end

Сущность User

class User < Principal
end

Сущность Role

class Role < Principal
end

Сущность Group

class Group < Principal
end

Как видим в объявлениях классов модели нет ничего особенного. Обо всем остальном позаботится ActiveRecord.

Примеры работы с моделью


Тепрь можно создавать наши объекты, сохранять в базу данных и выполнять запросы. В качестве примеров приведу тесты модели (они простые, так что прошу не бить ногами). Примеры только для Role и Group, с User-ом аналогично.

Фикстуры для principals



users_role:
id: 100
name: users
description: system users
type: Role

testers_group:
id: 101
name: testers
description: testers group
type: Group

Тест для сущности Role



require 'test_helper'

class RoleTest < ActiveSupport::TestCase
fixtures :principals

test "role_has_type_eq_role_after_save" do
role = Role.new(:name=>"admins",
:description=>"system administrators")
role.save

assert role.id
assert_equal "Role", role.type
end

test "users_role_exists_in_db" do
role = Role.find_by_id(100);

assert_not_nil role
assert_equal 100, role.id
assert_equal "users", role.name
assert_equal "Role", role.type
end
end

Тест для сущности Group



require 'test_helper'

class GroupTest < ActiveSupport::TestCase
fixtures :principals

test "group_has_type_eq_group_after_save" do
group = Group.new(:name=>"developers",
:description=>"developers group")
group.save

assert group.id
assert_equal "Group", group.type
end

test "users_group_exists_in_db" do
group = Group.find_by_id(101);

assert_not_nil group
assert_equal 101, group.id
assert_equal "testers", group.name
assert_equal "Group", group.type
end

end

Как видим все достаточно прозрачно.

Ложка дегтя


Как уже можно было догадаться есть и обратная сторона медали. Контроль соответствия набора значащих полей конкретной сущности лежит целиком на совести программиста. Для среды ActiveRecord значение имеет лишь колонка type. Так, что я вполне могу создать сущность Role с данными User-а. А это не очень хорошо отразится на отношении ко мне
моих коллег. Поэтому в Rails очень важно покрытие кода функциональными тестами.

Жду отзывов, исправлений и предложений.

No comments: