Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

duTchman1990's avatar

DB design and ORM

So I have this crm project where each user has specific area acceess eg. certain state access. I have splited that data to 4 different tables including users table where other 3 cotains user id as foreign key. Simple hasMany relation, but to avoid stroring of large data for each each I do not stored no data in district table which is the 4th table if the user has all state access.

table are in follwoing order users user_zone user_state user_district

For example if user id 1 has 1 zone access, 1 full state access then data will be stored in user_zone as user_id = 1 and zone_id = 1 single row and same in user_state single row but user_district do not have any row.

in second scenario user id 2 has 1 zone access, 1 full state access then data will be stored in user_zone as user_id = 2 and zone_id = 1 single row and same in user_state and user_district.

Problem: While searching from data with user has specific state and district I don't get user 1 is list while it's valid. I have added one macro like below

Builder::macro('user_area_filter', function ($parameters) {
			return $this->distinct('users.id')
				->with('get_district', 'get_state')
				->get()
				->map(function ($user) {
					if ($user->get_district->count() == 0) {
						$user->get_district = \Districts::whereIn('state_id', $user->get_state->pluck('state_id'))->get();
					}
					return $user;
				})
				->map(function ($user) use ($parameters) {
					$user->get_states_count    = $user->get_state->whereIn('state_id', $parameters['states'])->count();
					$user->get_districts_count = $user->get_district->whereIn('district_id', $parameters['districts'])->count();
					$user->get_state           = $user->get_state->toArray();
					$user->get_district        = $user->get_district->toArray();
					$user->valid               = 0;
					if ($user->get_states_count > 0 && $user->get_districts_count > 0) {
						$user->valid = 1;
						$columns     = array_merge($parameters['columns'], ['valid', 'get_state', 'get_district']);
						return $user->only($columns);
					}
				})->where('valid', 1);
		});
->with('get_district', 'get_state')
//these are hasMany relation from Users model
\Districts
//is the master district table
//full query
$query = User::where('is_active', 1)->select('id', 'name', 'official_mobile', 'email', 'profile_id', 'is_active', 'app_version', 'last_login')->whereHas('get_user_data', function ($q) {
				$q->where('work_on', 1);
			})
				->user_area_filter([ //macro
					'columns'   => ['id', 'name', 'official_mobile', 'email', 'profile_id', 'is_active', 'app_version', 'last_login'],
					'states'    => $states,
					'districts' => $districts
				]);
			$totCount = $query->count();
			$models   = $query->take(200)->skip(0);

above code just works but I am not sure how it'll perform in long run. Here is a snapshot of DB schema https://nimb.ws/iWTBKX

0 likes
6 replies
rodrigo.pedra's avatar
$allDistrictsIdsFromStates = Districts::query()
    ->whereIn('state_id', $states)
    ->pluck('district_id');

//full query
$query = User::query()
    ->where('is_active', 1)
    ->whereHas('get_user_data', fn ($relation) => $relation->where('work_on', 1))
    ->whereHas('get_state', fn ($relation) => $relation->whereIn('state_id', $states))
    ->where(
        fn ($query) => $query
            ->orWhereHas(
                'get_district',
                fn ($relation) => $relation->whereIn('district_id', $districts)
            )
            ->orWhereDoesntHave(
                'get_district',
                fn ($relation) => $relation->whereIn('district_id', $allDistrictsIdsFromStates)
            )
    );

$totCount = $query->count();
$models = $query->take(200)->skip(0)->get([
    'id',
    'name',
    'official_mobile',
    'email',
    'profile_id',
    'is_active',
    'app_version',
    'last_login',
]);

I skipped the macro all together. Also avoid using macros for queries specific to a model. Prefer local scopes, or a dedicated eloquent query builder.

References:

1 like
duTchman1990's avatar

@rodrigo.pedra let me check. I didn't understand one thing though $allDistrictsIdsFromStates should be applicable when relation user_district is empty eg when user has access to all district. In that case data will be available upto user_states table. Do you think it's bad design? Honestly previously these data were stored in json columns and it was slower.

1 like
rodrigo.pedra's avatar

@duTchman1990

$allDistrictsIdsFromStates is used in this part:

->where(
    fn ($query) => $query
        ->orWhereHas(
            'get_district',
            fn ($relation) => $relation->whereIn('district_id', $districts)
        )
        ->orWhereDoesntHave(
            'get_district',
            fn ($relation) => $relation->whereIn('district_id', $allDistrictsIdsFromStates)
        )
);

which reads as:

  • Either the user has any districts specified on the $districts variable, provided as a filter
  • Or the user does not have any district records from the districts of the provided $states variable.

In other words:

  • If the user has any related records that matches the provided $districts variable, than they should be included
  • If the user does not have any district within the filtered states in the provided $states variable, that would mean, from your description, this user is allowed on all districts of that state, then they also should be included

If none of the conditions above are matched, then the user should not be included.

Hope it is clearer now.

1 like
rodrigo.pedra's avatar

@duTchman1990 about the database design: It is hard to tell.

I would need more knowledge about what you are trying to accomplish and also know more about your other tables, and how your Eloquent relations are set.

One thing that would help, in this particular query, is if the tbl_user_district had also the state_id key as a column. SO we could skip the District query done before the users query by doing something like this:

->where(
        fn ($query) => $query
            ->orWhereHas(
                'get_district',
                fn ($relation) => $relation->whereIn('district_id', $districts)
            )
            ->orWhereDoesntHave(
                'get_district',
                fn ($relation) => $relation->whereIn('state_id', $states)
            )
    );

Which would read: either the user has any of the filtered districts, or doesn't have any districts for the filtered states.

duTchman1990's avatar

@rodrigo.pedra this one makes more sense and state_id can be added in tbl_user_districts also. Let me explore both of them in staging before going live. I think both of them will fit my need. Thanks.

1 like
rodrigo.pedra's avatar

@resultoffice please don't spam other threads with links to a current thread you are needing help.

We might live in different time zones, all answers in the forum are done by volunteers on their free-time, and also today is Sunday, which is a rest day in many parts of the world.

It is reasonable that not everyone would be available to help you just in the time you want.

If you need someone to help you out at your will, and can't afford to wait for their availability, I advise you to hire someone to work for you, or hire a consultant to help you out. Just be sure to account the right terms of work, because even paid workers are expected to work on a schedule and not to be available 24 hours every day.

Please or to participate in this conversation.