We use cookies to enhance your experience on the site
CodeWorlds

Repository Pattern - organising the treasury

You already know entities and relations - the treasury has its shelves catalogued and it is clear what connects to what. But who actually reaches into it? If every service in the Empire wrote its own SQL, the same "find a legionary by name" operation would exist in five places, in five versions, with five different bugs.

The Romans solved this differently: the treasury is reached through an archivist. A service does not descend into the cellar with a torch - it tells the archivist what it needs, and he knows which shelf holds it. In code, that archivist is a repository: the layer that separates business logic from the way data is stored.

Three steps to a repository

TypeORM gives you a repository for free for every entity - you just have to ask. The path is always the same, and it is worth remembering as a sequence.

Step one: the entity. You met it in previous lessons - a class with the

@Entity()
decorator describing a shelf in the treasury. Without it there is nothing to archive.

Step two: registration in the module. We tell NestJS which entities to prepare archivists for:

1// legionary.module.ts
2@Module({
3  imports: [TypeOrmModule.forFeature([Legionary])],
4  providers: [LegionaryService],
5  controllers: [LegionaryController],
6})
7export class LegionaryModule {}

Note the name:

forFeature
, not
forRoot
. This difference is a frequent source of confusion, so let's separate them once and for all. You call
forRoot
once, in the root module - it configures the database connection for the whole application. You call
forFeature
in each module separately, listing only the entities that module actually uses. One sets the road to the treasury, the other assigns archivists to a particular cohort.

Step three: injection into the service. We receive the registered archivist in the constructor:

1@Injectable()
2export class LegionaryService {
3  constructor(
4    @InjectRepository(Legionary)
5    private legionaryRepo: Repository<Legionary>,
6  ) {}
7}

The

@InjectRepository(Legionary)
decorator tells NestJS which archivist we are asking for - because an application has many, one per entity. The type
Repository<Legionary>
is generic notation:
Repository
is the general kind of archivist, and
<Legionary>
narrows it to this one entity. Thanks to that, TypeScript knows
find()
will return legionaries rather than just anything - and will suggest their fields as you type.

Remember the order: entity,

forFeature
in the module,
@InjectRepository
in the service. Skipping the middle step produces an unknown-provider error at application startup - and it is the most common cause of that message.

Reading - find with options

The archivist takes orders in the form of an options object. Its most important key is

where
:

1async findAll(): Promise<Legionary[]> {
2  return this.legionaryRepo.find();
3}
4
5async findCenturions(): Promise<Legionary[]> {
6  return this.legionaryRepo.find({
7    where: { rank: 'Centurion' },
8    relations: ['centurion'],
9    order: { name: 'ASC' },
10  });
11}

find()
with no arguments brings everything. With an options object - it narrows. Beware of one detail many people trip on: conditions must sit inside the
where
key. Writing
find({ rank: 'Centurion' })
is not a filter - it is an unknown option which TypeORM ignores, and you will get every legionary instead of just the centurions. No error is raised, so it is easy to miss.

The

relations
key pulls in related entities - the repository's equivalent of
leftJoinAndSelect
from the Query Builder.
order
sorts. When you need a single record, you call
findOne({ where: { id } })
- it returns the entity, or
null
when nothing matches.

Writing - create and save

Creating a new legionary is a pair of methods, and it is worth understanding why two rather than one:

1async create(dto: CreateLegionaryDto): Promise<Legionary> {
2  const legionary = this.legionaryRepo.create(dto);
3  return this.legionaryRepo.save(legionary);
4}

create()
does not touch the database. It only builds an entity object in memory - copying the fields from the DTO and giving it the
Legionary
class, so that decorators, hooks and default values will work. Only
save()
sends it to the treasury and returns the saved entity, now carrying the
id
assigned by the database.

This split has a practical point: between

create
and
save
you can still change or check something.
save()
also has a second face - called on an entity that already has an
id
, it performs an update instead of an insert. One method, two behaviours, depending on whether the record exists.

Updating - preload

For changing an existing record there is

preload
, a method with a non-obvious name:

1async update(id: number, dto: UpdateLegionaryDto): Promise<Legionary> {
2  const legionary = await this.legionaryRepo.preload({ id, ...dto });
3
4  if (!legionary) {
5    throw new NotFoundException(`Legionary ${id} does not exist`);
6  }
7
8  return this.legionaryRepo.save(legionary);
9}

preload
fetches the record with the given
id
from the database, lays the DTO's fields over it and returns an entity ready to be saved - but saves nothing yet. Its advantage is what you do not have to do: fields absent from the DTO keep their current values, so a partial update will not wipe the rest of the record. When no record with that
id
exists,
preload
returns
undefined
- hence the check before saving. Only
save()
makes the change permanent.

Deleting - remove and delete

Finally, two deletion methods which do the same thing in different ways:

1async remove(id: number): Promise<void> {
2  const legionary = await this.legionaryRepo.findOne({ where: { id } });
3
4  if (!legionary) {
5    throw new NotFoundException(`Legionary ${id} does not exist`);
6  }
7
8  await this.legionaryRepo.remove(legionary);
9}

remove()
takes an entity - which is why we fetch it first. That costs an extra query, but in exchange you get certainty that the record existed, and you trigger the deletion hooks.
delete(id)
takes the bare
id
and erases it in a single query - faster, but with no check and no hooks. Choose deliberately, @name:
remove
where correctness and reacting to a missing record matter,
delete
where speed does.

Summary

The treasury has an archivist and nobody goes down to the cellar on their own:

  • a repository separates business logic from the way data is stored,
  • the path is always three steps: an entity with
    @Entity()
    ,
    TypeOrmModule.forFeature([Entity])
    in the module,
    @InjectRepository(Entity)
    in the service constructor,
  • forRoot
    configures the connection once for the application,
    forFeature
    assigns entities to a single module,
  • find()
    takes the
    where
    ,
    relations
    and
    order
    options - conditions outside the
    where
    key are silently ignored,
  • create()
    builds an entity in memory,
    save()
    writes it to the database and can update too,
  • preload()
    merges an existing record with your changes without saving - it returns
    undefined
    when the record is absent,
  • remove(entity)
    deletes more safely and with hooks,
    delete(id)
    faster and without them.

In the next lesson you will meet the Query Builder - the tool for questions the archivist cannot handle with

find()
alone. For now remember: a repository is the treasury's archivist - the service says what it needs, not how to take it off the shelf.

Go to CodeWorlds