Skip to content

Commit

Permalink
Merge pull request #895 from circulon/fix/has_one_through_not_working
Browse files Browse the repository at this point in the history
Fix has one through relationship not working
  • Loading branch information
josephmancuso authored Oct 26, 2024
2 parents 8d5da73 + d377d37 commit 55ff6dd
Show file tree
Hide file tree
Showing 6 changed files with 306 additions and 113 deletions.
2 changes: 2 additions & 0 deletions src/masoniteorm/query/QueryBuilder.py
Original file line number Diff line number Diff line change
Expand Up @@ -1969,6 +1969,8 @@ def _register_relationships_to_model(
def _map_related(self, related_result, related):
if related.__class__.__name__ == 'MorphTo':
return related_result
elif related.__class__.__name__ == 'HasOneThrough':
return related_result.group_by(related.local_key)

return related_result.group_by(related.foreign_key)

Expand Down
246 changes: 156 additions & 90 deletions src/masoniteorm/relationships/HasOneThrough.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ def __init__(
self.local_owner_key = local_owner_key or "id"
self.other_owner_key = other_owner_key or "id"

def __getattr__(self, attribute):
relationship = self.fn(self)[1]()
return getattr(relationship.builder, attribute)

def set_keys(self, distant_builder, intermediary_builder, attribute):
self.local_key = self.local_key or "id"
self.foreign_key = self.foreign_key or f"{attribute}_id"
Expand All @@ -34,17 +38,18 @@ def set_keys(self, distant_builder, intermediary_builder, attribute):
return self

def __get__(self, instance, owner):
"""This method is called when the decorated method is accessed.
"""
This method is called when the decorated method is accessed.
Arguments:
instance {object|None} -- The instance we called.
Arguments
instance (object|None): The instance we called.
If we didn't call the attribute and only accessed it then this will be None.
owner (object): The current model that the property was accessed on.
owner {object} -- The current model that the property was accessed on.
Returns:
object -- Either returns a builder or a hydrated model.
Returns
QueryBuilder|Model: Either returns a builder or a hydrated model.
"""

attribute = self.fn.__name__
self.attribute = attribute
relationship1 = self.fn(self)[0]()
Expand All @@ -57,43 +62,60 @@ def __get__(self, instance, owner):
if attribute in instance._relationships:
return instance._relationships[attribute]

result = self.apply_query(
return self.apply_relation_query(
self.distant_builder, self.intermediary_builder, instance
)
return result
else:
return self

def apply_query(self, distant_builder, intermediary_builder, owner):
"""Apply the query and return a dictionary to be hydrated.
Used during accessing a relationship on a model
def apply_relation_query(self, distant_builder, intermediary_builder, owner):
"""
Apply the query and return a dict of data for the distant model to be hydrated with.
Arguments:
query {oject} -- The relationship object
owner {object} -- The current model oject.
Method is used when accessing a relationship on a model if its not
already eager loaded
Returns:
dict -- A dictionary of data which will be hydrated.
Arguments
distant_builder (QueryBuilder): QueryBuilder attached to the distant table
intermediate_builder (QueryBuilder): QueryBuilder attached to the intermediate (linking) table
owner (Any): the model this relationship is starting from
Returns
dict: A dictionary of data which will be hydrated.
"""
# select * from `countries` inner join `ports` on `ports`.`country_id` = `countries`.`country_id` where `ports`.`port_id` is null and `countries`.`deleted_at` is null and `ports`.`deleted_at` is null
distant_builder.join(
f"{self.intermediary_builder.get_table_name()}",
f"{self.intermediary_builder.get_table_name()}.{self.foreign_key}",
"=",
f"{distant_builder.get_table_name()}.{self.other_owner_key}",
)

return self
dist_table = distant_builder.get_table_name()
int_table = intermediary_builder.get_table_name()

return (
distant_builder.select(
f"{dist_table}.*, {int_table}.{self.local_owner_key} as {self.local_key}"
)
.join(
f"{int_table}",
f"{int_table}.{self.foreign_key}",
"=",
f"{dist_table}.{self.other_owner_key}",
)
.where(
f"{int_table}.{self.local_owner_key}",
getattr(owner, self.local_key),
)
.first()
)

def relate(self, related_model):
dist_table = self.distant_builder.get_table_name()
int_table = self.intermediary_builder.get_table_name()

return self.distant_builder.join(
f"{self.intermediary_builder.get_table_name()}",
f"{self.intermediary_builder.get_table_name()}.{self.foreign_key}",
f"{int_table}",
f"{int_table}.{self.foreign_key}",
"=",
f"{self.distant_builder.get_table_name()}.{self.other_owner_key}",
).where(
f"{self.intermediary_builder.get_table_name()}.{self.local_key}",
getattr(related_model, self.local_owner_key),
f"{dist_table}.{self.other_owner_key}",
).where_column(
f"{int_table}.{self.local_owner_key}",
getattr(related_model, self.local_key),
)

def get_builder(self):
Expand All @@ -104,68 +126,139 @@ def make_builder(self, eagers=None):

return builder

def get_related(self, query, relation, eagers=None, callback=None):
builder = self.distant_builder
def register_related(self, key, model, collection):
"""
Attach the related model to source models attribute
Arguments
key (str): The attribute name
model (Any): The model instance
collection (Collection): The data for the related models
Returns
None
"""

related = collection.get(getattr(model, self.local_key), None)
model.add_relation({key: related[0] if related else None})

def get_related(self, current_builder, relation, eagers=None, callback=None):
"""
Get the data to hydrate the model for the distant table with
Used when eager loading the model attribute
Arguments
query (QueryBuilder): The source models QueryBuilder object
relation (HasOneThrough): this relationship object
eagers (Any):
callback (Any):
Returns
dict: the dict to hydrate the distant model with
"""

dist_table = self.distant_builder.get_table_name()
int_table = self.intermediary_builder.get_table_name()

if callback:
callback(builder)
callback(current_builder)

(self.distant_builder.select(f"{dist_table}.*, {int_table}.{self.local_owner_key} as {self.local_key}")
.join(
f"{int_table}",
f"{int_table}.{self.foreign_key}",
"=",
f"{dist_table}.{self.other_owner_key}",
))

if isinstance(relation, Collection):
return builder.where_in(
f"{builder.get_table_name()}.{self.foreign_key}",
return self.distant_builder.where_in(
f"{int_table}.{self.local_owner_key}",
Collection(relation._get_value(self.local_key)).unique(),
).get()
else:
return builder.where(
f"{builder.get_table_name()}.{self.foreign_key}",
getattr(relation, self.local_owner_key),
return self.distant_builder.where(
f"{int_table}.{self.local_owner_key}",
getattr(relation, self.local_key),
).first()

def query_where_exists(
self, current_query_builder, callback, method="where_exists"
):
query = self.distant_builder
def attach(self, current_model, related_record):
raise NotImplementedError(
"HasOneThrough relationship does not implement the attach method"
)

def attach_related(self, current_model, related_record):
raise NotImplementedError(
"HasOneThrough relationship does not implement the attach_related method"
)

getattr(current_query_builder, method)(
query.join(
f"{self.intermediary_builder.get_table_name()}",
f"{self.intermediary_builder.get_table_name()}.{self.foreign_key}",
def query_has(self, current_builder, method="where_exists"):
dist_table = self.distant_builder.get_table_name()
int_table = self.intermediary_builder.get_table_name()

getattr(current_builder, method)(
self.distant_builder.join(
f"{int_table}",
f"{int_table}.{self.foreign_key}",
"=",
f"{query.get_table_name()}.{self.other_owner_key}",
f"{dist_table}.{self.other_owner_key}",
).where_column(
f"{current_query_builder.get_table_name()}.{self.local_owner_key}",
f"{self.intermediary_builder.get_table_name()}.{self.local_key}",
f"{int_table}.{self.local_owner_key}",
f"{current_builder.get_table_name()}.{self.local_key}",
)
)

return self.distant_builder

def query_where_exists(self, current_builder, callback, method="where_exists"):
dist_table = self.distant_builder.get_table_name()
int_table = self.intermediary_builder.get_table_name()

getattr(current_builder, method)(
self.distant_builder.join(
f"{int_table}",
f"{int_table}.{self.foreign_key}",
"=",
f"{dist_table}.{self.other_owner_key}",
)
).when(callback, lambda q: (callback(q)))
.where_column(
f"{int_table}.{self.local_owner_key}",
f"{current_builder.get_table_name()}.{self.local_key}",
)
.when(callback, lambda q: (callback(q)))
)

def get_with_count_query(self, builder, callback):
query = self.distant_builder
def get_with_count_query(self, current_builder, callback):
dist_table = self.distant_builder.get_table_name()
int_table = self.intermediary_builder.get_table_name()

if not builder._columns:
builder = builder.select("*")
if not current_builder._columns:
current_builder.select("*")

return_query = builder.add_select(
return_query = current_builder.add_select(
f"{self.attribute}_count",
lambda q: (
(
q.count("*")
.join(
f"{self.intermediary_builder.get_table_name()}",
f"{self.intermediary_builder.get_table_name()}.{self.foreign_key}",
f"{int_table}",
f"{int_table}.{self.foreign_key}",
"=",
f"{query.get_table_name()}.{self.other_owner_key}",
f"{dist_table}.{self.other_owner_key}",
)
.where_column(
f"{builder.get_table_name()}.{self.local_owner_key}",
f"{self.intermediary_builder.get_table_name()}.{self.local_key}",
f"{int_table}.{self.local_owner_key}",
f"{current_builder.get_table_name()}.{self.local_key}",
)
.table(query.get_table_name())
.table(dist_table)
.when(
callback,
lambda q: (
q.where_in(
self.foreign_key,
callback(query.select(self.other_owner_key)),
callback(
self.distant_builder.select(self.other_owner_key)
),
)
),
)
Expand All @@ -174,30 +267,3 @@ def get_with_count_query(self, builder, callback):
)

return return_query

def attach(self, current_model, related_record):
raise NotImplementedError(
"HasOneThrough relationship does not implement the attach method"
)

def attach_related(self, current_model, related_record):
raise NotImplementedError(
"HasOneThrough relationship does not implement the attach_related method"
)

def query_has(self, current_query_builder, method="where_exists"):
related_builder = self.get_builder()

getattr(current_query_builder, method)(
self.distant_builder.where_column(
f"{current_query_builder.get_table_name()}.{self.local_owner_key}",
f"{self.intermediary_builder.get_table_name()}.{self.local_key}",
).join(
f"{self.intermediary_builder.get_table_name()}",
f"{self.intermediary_builder.get_table_name()}.{self.foreign_key}",
"=",
f"{self.distant_builder.get_table_name()}.{self.other_owner_key}",
)
)

return related_builder
11 changes: 0 additions & 11 deletions tests/mysql/relationships/test_has_many_through.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@

from src.masoniteorm.models import Model
from src.masoniteorm.relationships import (
has_one,
belongs_to_many,
has_many_through,
has_many,
)
from dotenv import load_dotenv

Expand Down Expand Up @@ -88,11 +85,3 @@ def test_or_where_doesnt_have(self):
sql,
"""SELECT * FROM `inbound_shipments` WHERE `inbound_shipments`.`name` = 'Joe' OR NOT EXISTS (SELECT * FROM `countries` INNER JOIN `ports` ON `ports`.`country_id` = `countries`.`country_id` WHERE `inbound_shipments`.`from_port_id` = `ports`.`port_id`) AND `inbound_shipments`.`name` = 'USA'""",
)

def test_has_one_through_with_count(self):
sql = InboundShipment.with_count("from_country").to_sql()

self.assertEqual(
sql,
"""SELECT `inbound_shipments`.*, (SELECT COUNT(*) AS m_count_reserved FROM `countries` INNER JOIN `ports` ON `ports`.`country_id` = `countries`.`country_id` WHERE `inbound_shipments`.`from_port_id` = `ports`.`port_id`) AS from_country_count FROM `inbound_shipments`""",
)
Loading

0 comments on commit 55ff6dd

Please sign in to comment.