Как стать автором
Обновить

Single Table Inheritance in Ruby on Rails

Недавно, при поптировании одного приложения с Java на RoR столкнулся с проблемой «отображения иерархии классов на реляционную модель». Изветное дело в Java или .NET я бы использовал Hibernate/NHibernate и Single Table Inheritance или STI (в данном случае это наиболее подходящий из 3 представленных способов). На мое счастье, оказывается ActiveRcord в 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

# Save Role in database.
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

# Save Role in database.
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 очень важно покрытие кода функциональными тестами.

Жду отзывов, исправлений и предложений. ;)
Теги:
Хабы:
Данная статья не подлежит комментированию, поскольку её автор ещё не является полноправным участником сообщества. Вы сможете связаться с автором только после того, как он получит приглашение от кого-либо из участников сообщества. До этого момента его username будет скрыт псевдонимом.