Mangatsu / server

🌕 Media server for storing, tagging and viewing doujinshi, manga, art collections and other galleries with API and user control. Written in Go.
GNU General Public License v3.0
48 stars 6 forks source link

Create models for the database types (for goqu) #15

Open CrescentKohana opened 2 years ago

CrescentKohana commented 2 years ago

We could probably use the types Jet generated as a base: pkg/types/model.

karmek-k commented 2 years ago

Going to work on this right now.

CrescentKohana commented 2 years ago

Going to work on this right now.

Great! I have some preliminary stuff done in #25.

I ran into an issue with the Combined model and was not able get goqu to scan stuff correctly into this model:

type CombinedLibrary struct {
    Library
    Galleries []Gallery `db:"gallery"`
}

Expected:

[
  {
    "ID": 1,
    "Path": "test/",
    "Layout": "structured",
    "Galleries": [ <Gallery Object>, <Gallery Object>, ... ]
  },
  ...
]

Actual:

[
  {
    "ID": 1,
    "Path": "test/",
    "Layout": "structured",
    "Galleries": null
  },
  ...
]
karmek-k commented 2 years ago

I've just added db tags to all models (https://github.com/karmek-k/server/commit/6d136735f8dab2d873e95d8c4b4179e5aea688c9).

Regarding the CombinedLibrary issue, how are you scanning data from the db? Are you using GetLibraries?

Btw, PostgreSQL supports arrays as a column type, but MySQL does not.

CrescentKohana commented 2 years ago

Regarding the CombinedLibrary issue, how are you scanning data from the db? Are you using GetLibraries?

Yes, I tried to use GetLibraries with the ways listed on gogu docs.

Btw, PostgreSQL supports arrays as a column type, but MySQL does not.

Libraries are in one-to-many relationship with Galleries, and are being mapped/scanned to array here. No array columns used nor needed.

CrescentKohana commented 2 years ago

Actually I got it to work in really verbose way and there would probably be issues if two or more tables had columns named same.

Basically I had to make a new type which has all the columns on the same level, and scan them manually with Scanner() to a map which would then be converted into to struct we actually want.

There has to be a better way though. Even the documentation is saying this:

goqu also supports scanning into multiple structs. In the example below we define a Role and User struct that could both be used individually to scan into. However, you can also create a new struct that adds both structs as fields that can be populated in a single query.

type LibraryRow struct {
    ID              int32     `db:"id"`
    Path            string    `db:"path"`
    Layout          string    `db:"layout"`
    UUID            string    `db:"uuid"`
    LibraryID       int32     `db:"library_id"`
    ArchivePath     string    `db:"archive_path"`
    Title           string    `db:"title"`
    TitleNative     *string   `db:"title_native"`
    TitleTranslated *string   `db:"title_translated"`
    Category        *string   `db:"category"`
    Series          *string   `db:"series"`
    Released        *string   `db:"released"`
    Language        *string   `db:"language"`
    Translated      *bool     `db:"translated"`
    Nsfw            bool      `db:"nsfw"`
    Hidden          bool      `db:"hidden"`
    ImageCount      *int32    `db:"image_count"`
    ArchiveSize     *int32    `db:"archive_size"`
    ArchiveHash     *string   `db:"archive_hash"`
    Thumbnail       *string   `db:"thumbnail"`
    CreatedAt       time.Time `db:"created_at"`
    UpdatedAt       time.Time `db:"updated_at"`
}

func GetLibraries() ([]model.CombinedLibrary, error) {
    scanner, err := database.QB().
        From("library").
        Join(
            goqu.T("gallery"),
            goqu.On(goqu.I("gallery.library_id").Eq(goqu.I("library.id"))),
        ).
        Executor().
        Scanner()

    if err != nil {
        log.Error(err)
        return nil, err
    }

    defer func(scanner exec.Scanner) {
        if err := scanner.Close();  err != nil {
            log.Error(err)
        }
    }(scanner)

    librariesMap := make(map[int32]model.CombinedLibrary)
    for scanner.Next() {
        lr := LibraryRow{}
        if err = scanner.ScanStruct(&lr); err != nil {
            log.Error(err)
            return nil, err
        }

        var gallery = model.Gallery{UUID: lr.UUID,
            Title:           lr.Title,
            TitleNative:     lr.TitleNative,
            TitleTranslated: lr.TitleTranslated,
            Category:        lr.Category,
            Series:          lr.Series,
            Released:        lr.Released,
            Language:        lr.Language,
            Translated:      lr.Translated,
            Nsfw:            lr.Nsfw,
            Hidden:          lr.Hidden,
            ImageCount:      lr.ImageCount,
            ArchiveSize:     lr.ArchiveSize,
            ArchiveHash:     lr.ArchiveHash,
            Thumbnail:       lr.Thumbnail,
            CreatedAt:       lr.CreatedAt,
            UpdatedAt:       lr.UpdatedAt,
        }

        value, ok := librariesMap[lr.ID]
        if ok {
            value.Galleries = append(value.Galleries, gallery)
            librariesMap[lr.ID] = value
        } else {
            librariesMap[lr.ID] = model.CombinedLibrary{
                Library: model.Library{
                    ID:     lr.ID,
                    Path:   lr.Path,
                    Layout: lr.Layout,
                },
                Galleries: []model.Gallery{gallery},
            }
        }
    }

    librariesSlice := make([]model.CombinedLibrary, 0, len(librariesMap))
    for _, val := range librariesMap {
        librariesSlice = append(librariesSlice, val)
    }

    return librariesSlice, nil
}