Skip to content

[NEW FEATURE]Dynamic Multi-Schema Query Support with Prioritization #513

@lduf

Description

@lduf

Description

This feature aims to enable dynamic querying across multiple database schemas (e.g., GLOBAL, SPE) while respecting a defined priority order. Each model should define a databaseScope property, listing the schemas to query in priority order. For example:

  • databaseScope = ['global', 'team'] queries both GLOBAL and team-specific schemas, with GLOBAL taking precedence.
  • Queries dynamically adapt to team_id for identifying the specific schema.

The system must allow developers to use Eloquent-style methods (all, find, where, etc.) transparently, while the underlying logic manages multi-schema querying, data merging, and conflict prioritization.

Key Features:

  1. Query data from multiple schemas dynamically using team_id.
  2. Merge data from the schemas based on priority order.
  3. Extend Eloquent functionality to support this behavior without disrupting standard query patterns.
  4. Implement prioritization logic for cases where data exists in multiple schemas.
  5. Ensure relations (hasMany, belongsTo, etc.) respect the same priority and query logic.

Tasks

  1. Define a Multi-Schema Model Base Class:

    • Create a parent class (e.g., MultiSchemaModel) extending Eloquent.
    • Add a databaseScope property to define the schemas to query.
    • Implement methods for:
      • Fetching all data across schemas (allFromScopes).
      • Applying filters dynamically (queryFromScopes).
      • Handling prioritization during data merging (allWithPriority).
  2. Override Core Eloquent Query Methods:

    • all():
      • Query all records across the schemas defined in databaseScope.
      • Merge the results respecting the priority order.
    • find():
      • Retrieve a single record by ID, searching across all schemas in the defined order.
    • where():
      • Apply query filters dynamically on data from multiple schemas.
    • Ensure the overrides remain transparent for users.
  3. Implement Multi-Schema Support for Relations:

    • Extend Eloquent relations (hasMany, belongsTo, etc.) to respect the databaseScope.
    • Provide utilities like hasManyUsingScopes to simplify querying related data across schemas.
    • Test cases with nested relationships.
  4. Dynamic Schema Resolution Using team_id:

    • Detect the correct schema dynamically for a team_id (e.g., team_1 for team_id=1).
    • Update database connections dynamically for team-specific queries.
  5. Develop Sample Models:

    • Define examples such as:
      • Country: Queries both GLOBAL and team-specific schemas.
      • Item: Queries team-specific schema only.
  6. Write Tests:

    • Validate querying data from multiple schemas.
    • Test prioritization logic when merging data.
    • Cover edge cases:
      • Table exists only in GLOBAL.
      • Table exists only in SPE.
      • Conflicting data between schemas.
  7. Document the System:

    • Provide clear documentation for using the MultiSchemaModel base class.
    • Include usage examples for controllers and services.

Code Suggestions

1. Core MultiSchemaModel Class
This parent class will manage multi-schema querying and data merging.

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;

abstract class MultiSchemaModel extends Model
{
    protected static $databaseScope = ['global'];

    /**
     * Fetch all data across defined scopes.
     * @param int|null $teamId
     * @return \Illuminate\Support\Collection
     */
    public static function allFromScopes($teamId = null)
    {
        $results = collect();

        foreach (static::$databaseScope as $scope) {
            $connection = $scope === 'team' && $teamId ? "team_{$teamId}" : $scope;
            $table = (new static)->getTable();

            $data = DB::connection($connection)->table($table)->get();
            $results = $results->merge($data);
        }

        return $results;
    }

    /**
     * Query with filters across defined scopes.
     * @param callable|null $callback
     * @param int|null $teamId
     * @return \Illuminate\Support\Collection
     */
    public static function queryFromScopes($callback = null, $teamId = null)
    {
        $query = collect();

        foreach (static::$databaseScope as $scope) {
            $connection = $scope === 'team' && $teamId ? "team_{$teamId}" : $scope;
            $table = (new static)->getTable();

            $data = DB::connection($connection)->table($table);
            if ($callback) {
                $data = $callback($data);
            }

            $query = $query->merge($data->get());
        }

        return $query;
    }

    /**
     * Merge data with prioritization.
     * @param int $teamId
     * @return \Illuminate\Support\Collection
     */
    public static function allWithPriority($teamId)
    {
        $globalData = DB::connection('global')->table((new static)->getTable())->get();
        $specificData = DB::connection("team_{$teamId}")->table((new static)->getTable())->get();

        $mergedData = $specificData->map(function ($item) use ($globalData) {
            $globalItem = $globalData->firstWhere('id', $item->id);
            return $globalItem ?: $item; // Prioritize GLOBAL over SPE
        });

        return $globalData->merge($mergedData);
    }
}
  • Override Methods:
    • all(): Fetch data across schemas and merge results transparently.
  public static function all($columns = ['*'])
  {
      return static::allFromScopes(); 
  }
  • find(): Retrieve a single record by ID from all schemas.
public static function find($id, $columns = ['*'])
{
   $results = static::queryFromScopes(function ($query) use ($id, $columns) {
       return $query->where('id', $id);
   });

   return $results->first();
}
  • where(): Apply query filters and merge results dynamically.
public static function where($column, $operator = null, $value = null, $boolean = 'and')
{
    return static::queryFromScopes(function ($query) use ($column, $operator, $value, $boolean) {
        return $query->where($column, $operator, $value, $boolean);
    });
}
  • Dynamic Relationships:
    • Add methods like hasManyUsingScopes to manage relations across schemas.

2. Example Usage in Models
Define models with their databaseScope

  • Country Model:
    namespace App\Models;
    
    class Country extends MultiSchemaModel
    {
        protected static $databaseScope = ['global', 'team'];
        protected $table = 'countries';
    
    }

3. Example Usage in Controllers
Querying data remains transparent for developers.

use App\Models\Country;

$countries = Country::all(); // GLOBAL + SPE
$filteredCountries = Country::where('name', 'like', '%France%')->get(); // GLOBAL + SPE with filter
$country = Country::find(42); // Search by ID in GLOBAL + SPE

Discussion

  1. Performance Implications:

    • How to handle large datasets when merging results?
    • Should we explore database-level optimizations (e.g., UNION ALL) for faster queries?
  2. Conflict Resolution:

    • Should GLOBAL always take precedence, or should this be configurable?
    • How to handle cases where SPE overrides GLOBAL (future extension)?
  3. Testing Strategy:

    • Ensure coverage for edge cases (e.g., missing tables, conflicting data).
    • Benchmark performance with significant data volumes across schemas.
  4. Caching Options:

    • Can frequently queried data (e.g., list_countries) be cached to improve performance?

Deliverables

  1. Fully functional MultiSchemaModel class with dynamic schema support.
  2. Example models and controllers demonstrating usage.
  3. Test coverage for all cases.
  4. Documentation for developers.

Related Issues

Blocked by #512.

Metadata

Metadata

Assignees

No one assigned

    Labels

    AI-issueWhen applied, trigger AI help to summarize issueNew featureNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    Status

    Todo

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions