Pull to refresh

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

Жду отзывов, исправлений и предложений. ;)
Tags:
Hubs:
You can’t comment this publication because its author is not yet a full member of the community. You will be able to contact the author only after he or she has been invited by someone in the community. Until then, author’s username will be hidden by an alias.