rel

repository·master·Indexed 21 days ago

https://github.com/go-rel/rel

A modern database access layer for Golang designed for layered architectures. It provides an ORM-like experience featuring an extendable query builder, seamless nested transaction management, and a built-in reltest package for repository testing. It supports eager loading, composite primary keys, soft deletion, pagination, and multi-adapter infrastructure with schema migration capabilities.

Tokens
25.4K
Snippets
112
Records
135
Agent score
73%

What's inside rel

  1. Overview of REL features

    master

    REL is a modern, ORM-ish database access layer designed for layered architectures in Golang. Key features include:

    • Testability: Built-in reltest package for testing repositories.
    • Transactions: Supports seamless nested transactions.
    • Query Building: An elegant, extendable query builder that supports both builder syntax and plain SQL.
    • Data Management: Supports Eager loading, Composite Primary Keys, Soft Deletion, and Pagination.
    • Infrastructure: Multi-adapter support and Schema Migration capabilities.
  2. Use Structset for repository mutations

    master

    A Structset is a mutator used for repository insert or update operations. It saves every field in a struct and its associations, provided they are loaded. This is the default mutator used by the repository.

    When applying a Structset via Apply(doc, mut):

    • It automatically handles timestamp fields like created_at, inserted_at, and updated_at if the document has the corresponding flags (HasCreatedAt, HasUpdatedAt).
    • It supports cascading mutations to associations if mut.Cascade is set to true.
    • It can be configured to skip zero values for non-primary fields.
    // Example conceptual usage
    ss := rel.NewStructset(myEntity, true)
    ss.Apply(targetDoc, mutation)
  3. Configure database field names using `db` tags

    master

    REL uses the db struct tag to map Go fields to database columns.

    • Custom Name: Use db:"column_name" to specify a custom name.
    • Ignore Field: Use db:"-" to exclude a field from metadata.
    • Embedded Structs: Use db:",embedded" to treat a struct as an embedded part of the parent document, merging its fields into the parent's namespace.
    • Primary Key: Use db:",primary" to explicitly mark a field as a primary key.

    If no db tag is provided, the field name is converted from CamelCase to snake_case.

    type User struct {
        ID        int       `db:"id,primary"` 
        FirstName string    `db:"first_name"` 
        Password  string    `db:"-"` // Ignored
        Metadata  Meta      `db:",embedded"` 
    }
    
    type Meta struct {
        LoginCount int `db:"login_count"` 
    }
  4. Create a new index with IndexOption

    master
    While createIndex and createUniqueIndex are internal helpers in this file, the Index struct and the IndexOption interface allow for defining index creation and dropping operations. You can use IndexOption implementations to apply metadata like comments or specific database options to an index definition.
  5. Combine filters using And() and Or()

    master

    You can combine multiple FilterQuery objects into a single logical group using the And and Or functions, or by using the method-chaining style on an existing FilterQuery.

    Functional Style:

    filter := rel.And(rel.Eq("a", 1), rel.Eq("b", 2))
    filter := rel.Or(rel.Eq("a", 1), rel.Eq("b", 2))

    Chaining Style:

    filter := rel.Eq("a", 1).And(rel.Eq("b", 2))
    filter := rel.Eq("a", 1).Or(rel.Eq("b", 2))
    // Using And/Or functions
    f1 := rel.And(rel.Eq("field1", "val1"), rel.Eq("field2", "val2"))
    
    // Using method chaining
    f2 := rel.Eq("field1", "val1").And(rel.Eq("field2", "val2"))
  6. Configure associations using struct tags

    master

    Associations in rel are defined using Go struct tags. The library uses these tags to determine how entities relate to one another.

    Supported Tags

    • ref: The name of the reference field on the target document.
    • fk: The name of the foreign key field on the current document.
    • through: The name of the intermediary association for many-to-many or complex relationships.
    • auto or autoload: Set to "true" to enable automatic loading of the association when the parent is loaded.
    • autosave: Set to "true" to enable automatic saving of the association when the parent is modified.

    Tag Inference Behavior

    If ref or fk are not explicitly provided, the library attempts to guess them:

    • For through associations, it defaults to "id" for both.
    • If a BelongsTo relationship is detected via naming conventions (e.g., fieldname_id), it infers the fields accordingly.
    • Otherwise, it defaults ref to "id" and fk to the snake_case version of the type name plus "_id" (e.g., User becomes user_id).

    Note: autosave is not supported for has one or has many through associations and will cause a panic if configured.

  7. How Query composition and merging works

    master

    REL uses a composition model for queries. The Build method on a Query object allows merging one query into another.

    When merging:

    1. If the target query is empty, it is replaced by the new query.
    2. If the target query already has data, fields are merged:
      • Table and SelectQuery are overwritten if provided.
      • JoinQuery and SortQuery are appended.
      • WhereQuery is combined using And logic.
      • GroupQuery is overwritten.
      • OffsetQuery and LimitQuery are overwritten.
      • LockQuery is overwritten.
      • ReloadQuery, CascadeQuery, and UsePrimaryDb use boolean OR logic.
  8. Use SubQuery to create nested query expressions

    master

    A SubQuery wraps an existing Query with a prefix, typically used to create nested expressions like Prefix(sub-query). This is useful for constructing complex logical expressions that some databases represent using keywords like ALL or ANY.

    // Example of the conceptual structure
    sub := rel.SubQuery{
        Prefix: "PREFIX",
        Query:  someQuery,
    }
  9. Use Changeset to track and apply field updates

    master

    A Changeset is a mutator used to efficiently perform update operations by identifying only the fields and associations that have changed compared to a snapshot of the original entity.

    Key behaviors:

    • Memory Trade-off: Enabling a Changeset duplicates the original struct values in a snapshot, which consumes more memory but allows for precise delta tracking.
    • Equality Checking: It uses Equal(any) bool if the type implements it, otherwise it uses standard comparison, time.Time equality, or bytes.Equal for byte slices.
    • Cascading: When calling Apply, if the resulting Mutation has Cascade set to true, changes in associated documents (BelongsTo, HasOne, HasMany) will also be processed and added to the mutation.
    • Automatic Timestamps: If the document has the HasUpdatedAt flag, Apply will automatically add a Set("updated_at", Now()) operation if any fields were mutated.
    // Create a changeset for an entity
    changeset := rel.NewChangeset(myEntity)
    
    // ... modify myEntity ...
    
    // Get a map of all changes
    changes := changeset.Changes()
    
    // Or apply changes directly to a mutation
    mut := &rel.Mutation{}
    changeset.Apply(rel.NewDocument(myEntity), mut)
  10. Initialize a Repository with New()

    master

    To start using REL, create a new Repository instance by passing an Adapter to the New function. The repository handles high-level data access, instrumentation, and transaction management.

    import "github.com/go-rel/go-rel/rel"
    
    // Assuming 'adapter' is an already initialized rel.Adapter
    repo := rel.New(adapter)
  11. Use GroupQuery to build complex HAVING clauses

    master

    You can combine multiple filters within a GroupQuery using Having or OrHaving. OrHaving specifically wraps the provided filters in an AND before applying an OR to the existing filter chain, allowing for complex logical grouping in the HAVING clause.

    // Example: GROUP BY department HAVING (salary > 50000 AND age < 30) OR (bonus > 1000)
    // Note: Exact syntax depends on your FilterQuery implementation
    
    group := rel.NewGroup("department").
        OrHaving(
            rel.And(
                rel.Gt("salary", 50000),
                rel.Lt("age", 30),
            ),
            rel.Gt("bonus", 1000),
        )
    
    query := &rel.Query{}
    group.Build(query)