Lompat ke konten
dokumentasi.id
Commit 43aad26
v1
Star1,1 rb

Kamu membaca versi lama. Halaman ini hanya bisa dibaca.

6

Lapisan Repository

Lapisan repository adalah satu-satunya tempat di proyek ini yang menyusun query GORM. Setiap struct repository menempelkan generic Repository[T] untuk operasi CRUD dasar, lalu menambahkan method yang benar-benar dibutuhkan entity-nya. Halaman ini memetakan setiap method ke operasi GORM yang dijalankannya dan ke scopes yang menyaring hasil pencarian.

Satu generic untuk tiga entity

Repository[T] hanya menyimpan satu field, DB *gorm.DB, dan tidak menyimpan handle transaksi apa pun. Tiga struct turunan menyematkan generic itu dengan parameter entity masing-masing: UserRepository memakai Repository[entity.User], ContactRepository memakai Repository[entity.Contact], dan AddressRepository memakai Repository[entity.Address] internal/repository/repository.go:5-7 internal/repository/user_repository.go:10-23 internal/repository/contact_repository.go:11-14 internal/repository/address_repository.go:10-31. Ketiga konstruktor NewUserRepository, NewContactRepository, dan NewAddressRepository hanya menerima *logrus.Logger, lalu dipanggil sekali saat bootstrap internal/config/app.go:28-32.

Karena generic itu disematkan, method seperti Create dan FindById otomatis tersedia pada ketiga repository tanpa ditulis ulang, dan tabel tujuan ditentukan oleh TableName() milik entity: users, contacts, addresses internal/entity/user_entity.go:4-16.

Pemetaan method ke operasi GORM

graph TD
  subgraph G["Repository[T] generic"]
    Create["Create(db, entity)"]
    Update["Update(db, entity)"]
    Delete["Delete(db, entity)"]
    CountById["CountById(db, id)"]
    FindById["FindById(db, entity, id)"]
  end
  subgraph C["Method khusus"]
    Token["UserRepository.FindByToken"]
    Owner["ContactRepository.FindByIdAndUserId"]
    AddressOwner["AddressRepository.FindByIdAndContactId"]
    AddressList["AddressRepository.FindAllByContactId"]
    Search["ContactRepository.Search"]
  end
  Create --> O1["db.Create"]
  Update --> O2["db.Save"]
  Delete --> O3["db.Delete"]
  CountById --> O4["db.Model + Where + Count"]
  FindById --> O5["db.Where + Take"]
  Token --> O6["WHERE token = ?"]
  Owner --> O7["WHERE id = ? AND user_id = ?"]
  AddressOwner --> O8["WHERE id = ? AND contact_id = ?"]
  AddressList --> O9["WHERE contact_id = ?"]
  Search --> O10["FilterContact + Offset + Limit"]
  style Create fill:#f59e0b,color:#000
  style Update fill:#f59e0b,color:#000
  style Delete fill:#f59e0b,color:#000
  style CountById fill:#f59e0b,color:#000
  style FindById fill:#f59e0b,color:#000
  style Token fill:#8b5cf6,color:#fff
  style Owner fill:#8b5cf6,color:#fff
  style AddressOwner fill:#8b5cf6,color:#fff
  style AddressList fill:#8b5cf6,color:#fff
  style Search fill:#8b5cf6,color:#fff
  style O4 fill:#336791,color:#fff
  style O5 fill:#336791,color:#fff
  style O7 fill:#336791,color:#fff
%% fig: Pemetaan method repository ke operasi GORM
%% desc: Method generic dan method khusus per entity diterjemahkan menjadi klausa GORM yang berbeda, dari INSERT sampai WHERE berlapis.
Fig. 1: Pemetaan method repository ke operasi GORMMethod generic dan method khusus per entity diterjemahkan menjadi klausa GORM yang berbeda, dari INSERT sampai WHERE berlapis.

Lima method generic adalah inti lapisan ini. Create memanggil db.Create, Update memanggil db.Save, Delete memanggil db.Delete, sedangkan CountById dan FindById menambahkan filter id sebelum menghitung atau mengambil satu baris internal/repository/repository.go:9-29.

MethodSignatureOperasi GORMEfek
Create(db *gorm.DB, entity *T) errordb.Create(entity)INSERT satu baris
Update(db *gorm.DB, entity *T) errordb.Save(entity)UPDATE seluruh kolom baris
Delete(db *gorm.DB, entity *T) errordb.Delete(entity)DELETE berdasarkan primary key
CountById(db *gorm.DB, id any) (int64, error)db.Model(new(T)).Where("id = ?", id).Count(&total)SELECT COUNT
FindById(db *gorm.DB, entity *T, id any) errordb.Where("id = ?", id).Take(entity)SELECT dengan LIMIT 1

Query khusus di atas generic

Tiga method tambahan tidak bisa diturunkan dari generic karena menyaring lebih dari satu kolom. UserRepository.FindByToken mencari token = ?, ContactRepository.FindByIdAndUserId memasangkan id dengan user_id, dan AddressRepository menyediakan dua pembacaan: FindByIdAndContactId serta FindAllByContactId yang mengembalikan slice internal/repository/user_repository.go:10-23 internal/repository/contact_repository.go:22-24 internal/repository/address_repository.go:10-31.

Pola id plus kolom pemilik itu penting: setiap pembacaan Contact dan Address ikut membawa user_id atau contact_id, sehingga kepemilikan baris diperiksa oleh query, bukan oleh pengecekan terpisah di usecase. Pemanggilnya pun selalu meneruskan identitas pemilik tersebut, misalnya ContactUseCase.Update dan AddressUseCase.Update internal/usecase/contact_usecase.go:84-87 internal/usecase/address_usecase.go:105-106.

RepositoryMethodKlausa penyaringPemanggil
UserRepositoryFindByTokentoken = ?UserUseCase saat autentikasi internal/usecase/user_usecase.go:49-52
UserRepositoryCountById (warisan generic)id = ?UserUseCase.Create untuk menolak ID duplikat internal/usecase/user_usecase.go:72-76
ContactRepositoryFindByIdAndUserIdid = ? AND user_id = ?ContactUseCase.Update internal/usecase/contact_usecase.go:84-87
ContactRepositorySearchscope FilterContactContactUseCase.Search internal/usecase/contact_usecase.go:183-187
AddressRepositoryFindByIdAndContactIdid = ? AND contact_id = ?AddressUseCase.Update internal/usecase/address_usecase.go:105-106
AddressRepositoryFindAllByContactIdcontact_id = ?AddressUseCase.List internal/usecase/address_usecase.go:203-207

⚠️ FindById memakai Take, bukan Find. Ketika baris tidak ada, GORM mengembalikan gorm.ErrRecordNotFound sebagai error biasa, dan usecase yang menerjemahkannya menjadi fiber.ErrNotFound seperti pada ContactUseCase.Update internal/repository/repository.go:27-29 internal/usecase/contact_usecase.go:84-87. Repository sendiri tidak pernah menelan error itu.

Pagination lewat scopes

Hanya ContactRepository yang punya pencarian berhalaman. Search menjalankan dua query terpisah: satu Find dengan Offset dan Limit untuk mengambil satu halaman, satu Count untuk total seluruh baris yang cocok. Keduanya memakai scope yang sama, FilterContact, sehingga total tidak pernah menyimpang dari isi halaman internal/repository/contact_repository.go:26-38.

graph LR
  UC["ContactUseCase.Search"] --> Repo["ContactRepository.Search"]
  Repo --> Scope["FilterContact(request)"]
  Scope --> Page["db.Scopes + Offset + Limit + Find"]
  Scope --> Total["db.Model + Scopes + Count"]
  Page --> Rows["[]entity.Contact"]
  Total --> Hitung["total int64"]
  Rows --> Ctrl["ContactController.Search"]
  Hitung --> Ctrl
  Ctrl --> Meta["PageMetadata"]
  style Scope fill:#f59e0b,color:#000
  style Page fill:#336791,color:#fff
  style Total fill:#336791,color:#fff
  style Rows fill:#8b5cf6,color:#fff
  style Hitung fill:#8b5cf6,color:#fff
  style Meta fill:#8b5cf6,color:#fff
%% fig: Dua query dari satu scope di ContactRepository.Search
%% desc: FilterContact dipakai ulang oleh query halaman dan query hitung, lalu keduanya bertemu kembali di controller untuk menyusun PageMetadata.
Fig. 2: Dua query dari satu scope di ContactRepository.SearchFilterContact dipakai ulang oleh query halaman dan query hitung, lalu keduanya bertemu kembali di controller untuk menyusun PageMetadata.

Offset dihitung sebagai (request.Page - 1) * request.Size, dan Limit memakai request.Size apa adanya internal/repository/contact_repository.go:26-38. Nilai Page dan Size berasal dari SearchContactRequest yang divalidasi min=1 untuk Page serta min=1,max=100 untuk Size internal/model/contact_model.go:31-38. Batas 100 itu yang menahan satu request menarik seluruh tabel.

Scope FilterContact sendiri menyusun klausa secara kondisional: user_id = ? selalu ditambahkan, sedangkan nama, telepon, dan email hanya ikut bila nilainya tidak kosong, masing-masing dengan pola %...% untuk LIKE internal/repository/contact_repository.go:40-61.

Field requestKondisiKlausa yang ditambahkan
UserIdselaluuser_id = ?
NameName != ""first_name LIKE ? OR last_name LIKE ? dengan pola %name%
PhonePhone != ""phone LIKE ? dengan pola %phone%
EmailEmail != ""email LIKE ? dengan pola %email%

Scope mengembalikan func(tx *gorm.DB) *gorm.DB, jadi ia bisa ditempelkan ke query apa pun. Itulah sebabnya query Count memakai db.Model(&entity.Contact{}) ditambah scope yang sama, bukan menghitung hasil Find di memori internal/repository/contact_repository.go:26-38. Angka total dari repository lalu dipakai controller untuk menghitung TotalPage dengan math.Ceil(float64(total) / float64(request.Size)) internal/delivery/http/contact_controller.go:62-67.

Kepemilikan transaksi ada di usecase

Setiap method repository menerima db *gorm.DB sebagai parameter pertama, bukan memakai r.DB. Konsekuensinya, repository tidak pernah tahu apakah ia sedang berjalan di dalam transaksi. Usecase yang membuka tx := c.DB.WithContext(ctx).Begin() beserta defer tx.Rollback(), meneruskan tx ke repository, lalu memanggil tx.Commit() sendiri internal/usecase/contact_usecase.go:38-39 internal/usecase/contact_usecase.go:55-58.

⚠️ Jangan mengubah satu method repository agar memakai r.DB langsung. Pemanggil saat ini selalu mengirim tx hasil Begin dari usecase, sehingga perubahan itu akan memecah atomisitas rangkaian FindByIdAndUserId lalu Update atau Delete yang sekarang berjalan dalam satu transaksi internal/usecase/contact_usecase.go:84-87.