Core » Structs
Relations return plain hashes by default, but it's more common to use simple objects with attribute readers to represent data in your application. These objects should be treated as pure data structures, that don't have to be mutated at run-time, and don't depend on any external systems (such as mailers, API clients, message buses etc.). Their methods should only deal with the attributes they encapsulate. It's also common to use structs as aggregate roots, in which case they may provide methods to simplify accessing data represented by aggregate children.
You can work with structs in 3 different ways:
- Rely on
auto_struct
without defining custom struct classes - this works very well in the beginning of a project, where you don't need your own methods at all - Use
auto_struct
with custom struct classes - a nice middle-ground where you benefit from dynamic mapping but you also have access to your own methods - Use custom objects - the most advanced technique, where data are mapped to your own objects. This approach should be used in mature projects where complete separation from persistence layer is going to be beneficial.
Notice that you can use all 3 ways at the same time, depending on what makes sense in a given use case
Auto-struct
Auto-struct is a relation feature which can automatically transform plain relation tuples to struct objects. These objects are instances of ROM::Struct
and have explicit attributes defined, based on information from relation schemas.
You can enable this feature via auto_struct(true)
in a relation class:
class Users < ROM::Relation[:sql]
schema(infer: true) do
associations do
has_many :tasks
end
end
auto_struct(true)
end
users.by_pk(1).one
=> #<User id=1 name="Jane">
users.by_pk(1).combine(:tasks).one
=> #<User id=1 name="Jane" tasks=[#<Task id=1 user_id=1 title="Jane's Task">]>
This feature is enabled by default in repositories.
Auto-struct with custom classes
Relations support configuring struct_namespace
, it is set to ROM::Struct
by default, which means struct classes are generated for you automatically within ROM::Struct
namespace. If you want to provide your own struct classes, simply put them in a module and configure it as the struct_namespace
.
Let's say you have Entities
namespace and would like to provide a custom Entities::User
class:
module Entities
class User < ROM::Struct
def full_name
"#{first_name} #{last_name}"
end
end
end
class Users < ROM::Relation[:sql]
schema(infer: true)
struct_namespace Entities
auto_struct true
end
user = users.by_pk(1).one
user.full_name
# => "Jane Doe"
How struct classes are determined
Mappers will look for struct classes based on Relation#name
, but this is not restricted
to canonical names of your relations, as they can be aliased too. For instance, you may
define :admins
relation, which is restricted to users with type
set to "Admin"
. Then
if you have a Entities::Admin
class, it will be used as the struct class for :admins
relation.
module Entities
class User < ROM::Struct
def admin?
false
end
end
class Admin < User
def admin?
true
end
end
end
class Admins < ROM::Relation[:sql]
dataset { where(type: "Admin") }
schema(:users, as: :admins, infer: true)
struct_namespace Entities
auto_struct true
end
admin = admins.by_pk(1).one
admin.admin?
# true
Usage with repositories
It is recommended to configure struct_namespace
in repositories, as it's the appropriate layer where application-specific data structures are coming from.
Mapping to custom objects
You can ask a relation to instantiate your own objects via Relation#map_to
interface.
Your object class must have a constructor which accepts a hash with attributes.
Here's a simple example:
class User
attr_reader :attributes
def initialize(attributes)
@attributes = attributes
end
def [](name)
attributes[name]
end
end
users.by_pk(1).map_to(User)
# => #<User:0x007fa7eabf1a50 @attributes={:id=>1, :name=>"Jane"}>