Doctrine inner join three tables - doctrine

I'm trying to get Posts by Tags in Symfony 4 with Doctrine. I have three tables like this:
Post
------------------------------
| id | title | content | ... |
------------------------------
Tag
-------------
| id | name |
-------------
TagPost (which makes the association between tags and posts)
--------------------
| tag_id | post_id |
--------------------
There can be several tags by posts and a tag can be used for several posts, that's why I use an association table.
I already succeeded to get it but only with raw sql, I have tried multiple times with the query builder and no way to get it. Any advices ?
The query (working):
"SELECT post.id, post.title, post.author_id, post.content, post.datetime,
post.tile FROM post
INNER JOIN tag_post ON post.id = tag_post.post_id
INNER JOIN tag ON tag_post.tag_id = tag.id
WHERE tag.id = " . $tag_id;

Assuming you're writing this inside a method in your PostRepository (https://symfony.com/doc/3.3/doctrine/repository.html), you'd write:
$qb = $this->createQueryBuilder('post')
->select('post.id, post.title, post.author_id, post.content, post.datetime, post.tile')
->innerJoin('post.tag', 't')
->where('t.id = :tagid')
->setParameter('tagid', $tag_id)
;
$result = $qb->getQuery()->getResult();
Several things to note:
The createQueryBuilder method is not exactly the same in a Repository and in an EntityManager. The EntityManager one needs a ->from() method, while the Repository one guesses the 'from' table and takes instead just a constructor argument for the alias.
The field names, like 'id', 'author_id', 'datetime' etc should not be the names of the fields in your database, but the names of the properties of your doctrine entities. They might be the same, but they're probably camelCase instead of snake_case (e.g., 'authorId'), or they could be completely different. Check your entities to make sure.
Similarly, I'm assuming that the $post entities have a $tag field properly defined via doctrine as a ManyToMany relation. If that's the case, Doctrine will know on its own how to join that property by its name, so that the ->innerJoin method will only need one additional parameter: the alias. If you could include your entities definitions, that would help troubleshoot any additional problems.
Is post.tile (at the end of the SELECT clause) on purpose or is it a misspelling of post.title?

Related

how retrieve relation between two model that other models have polymorphic relation with both of them

I have these two models: Tag and Translate, I have these tables: tags, translates, translatables, Tag model uses translatable.
my table columns is like this:
translates: translatables: tags:
id name lang | translate_id translatable_id translatable_type | id
-- ---- ---- | ------------- --------------- ----------------- | --
| |
I want to write a function to get current translation of tag based on app locale which means I should query on translates tables based on translatables table data and using translateble_id and translatable_type find id in the translate table and get the name based on lang.
do you know how i can do this?
this is translatable trait:
trait Translatable{
public function translates(){
return $this->morphToMany(Translate::class,'translatable','translatables','translatable_id','translate_id');
}
}
and this is how I get locale:
$locale=app()->getLocale();
I get tags translates with querybuilder like:
$locale=app()->getLocale();
$tags=DB::table('translates as T')
->where('lang','=',$locale)
->join('translatables as R','R.translate_id','=','T.id')
->where('R.translatable_type','=','App\Models\Tag')
->join('tags as M','R.translatable_id','=','M.id')
->get();
but i want to use a function in model and eloquent.

Joining multiple tables on an already joined table using Eloquent

I have the following which brings back all users in a group along with their posts.
$group = Group::where('id', $id)->with('users.posts')->firstOrFail();
However, what I need is an additional join on the users to bring back additional (hasMany) information.
What I want is something like this (although this doesn't work)
$group = Group::where('id', $id)->with('users.posts,houses')->firstOrFail();
The sql would look something like
SELECT * FROM groups
JOIN group_users ON groups.id = group_users.group_id
JOIN users ON users.id = group_users.user_id
JOIN posts ON posts.user_id = users.id
JOIN house_users ON house_users.user_id = users.id
JOIN houses ON houses.id = house_users.house_id
WHERE groups.id = 123
If you pass a single argument to with(), it will look for a relationship with a matching name. Using a single string with a comma won't work as it won't parse and respect it. Since you're trying to use multiple relationships, this needs to be multiple signature, which there are a couple ways to accomplish.
First, array syntax:
->with(["users.posts", "houses"])
Second, multiple arguments:
->with("users.posts", "houses")
Either method will specify that you want multiple relationships loaded to your initial query; preference is given to whichever you find easier to read.

Trying to achieve a hasManyThrough type relationship

I'm trying to achieve something that is similar to Laravel's hasManyThrough, but I'm not sure my DB is set up appropriately or I'm just missing something.
I am trying to display a page for admins to show all of the sites we support. I would like to have a simple column that shows a distinct count of how many customers are attached to each site. To do this, I was going to go through the orders table and retrieve a distinct list of users, then simply use the ->count() method inside my view.
Here is my DB setup (simplified):
sites table (primary key: 'id'):
id | ...
users table (primary key: 'id'):
id | first_name | last_name | ...
orders table (primary key: 'order'):
id | order | user_id | site_id | ....
Site model:
public function customers()
{
return $this->hasManyThrough('App\User', 'App\Order', 'site_id', ' id')->distinct();
}
I realize right away that the key difference between my DB setup and the documentation is I do not have an order_id in my users table, but it doesn't make sense that I do since a user can have many orders.
It is worth noting: I also have a table user_orders. I'm not sure if I should be using that instead. user_orders has the following set up:
id | user_id | order
You can see that it is simply an intermediate table to hold connections between users and orders (remember order is the PK in orders, not id).
So, can anyone help me understand what I am doing wrong?
You could get away with a Join. I give you this sample code to guide you
public function customers()
{
return $this->hasMany('App\Order')
->leftjoin('users', 'users.id', 'orders.user_id')
->groupBy('users.id'); //Is this needed?
//Above code will return you a collection of Order though, but with the user data.
//Let's try using the User model
return App\User::whereHas('orders', function($query) use ($this->id) {
$query->where('site_id', $this->id);
})->get();
}

Eloquent Relationship Between Two Unions

I have four models. TableFoo and TableFooArchive share a common schema (items for TableFoo are periodically moved to TableFooArchive). I also have TableBar and TableBarArchive. TableFoo may or may not have one related row in TableBar or TableBarArchive. TableFooArchive may or may not have one related row in TableBarArchive.
I'd like to be able, using Eloquent, to union TableFoo and TableFooArchive then eager load the union of TableBar and TableBarArchive. If I only had TableFoo and TableBar, I could just set up a hasOne on the model and be done. How can I achieve the same with all four tables?
My start is to have a repository with the following:
public function getData($id)
{
$a = TableFoo::selectRaw('
columnA,
columnB as column_b_abc,
columnB as colum_b_def,
created_at
')
->whereRaw("
id = $id,
and so forth...
");
$b = TableFooArchive::selectRaw('
columnA,
columnB as column_b_abc,
columnB as colum_b_def,
created_at
')
->whereRaw("
id = $id,
and so forth...
")
->unionAll($a)
->orderBy('created_at', 'DESC')
->get();
return $b;
}
My guess is that it's possible to define a method on each model that returns TableBarTableBarArchive. I'm just not sure how I would do that nor how I would pull it through on the above using ->with('unionedResults'). FWIW, I can't change the table structure to use two instead of four.
I feel like there's probably an obvious and simple solution that just isn't occurring to me at the moment. Any help or insight would be appreciated!
UPDATE:
What I was looking for was to lazy load the relationship. The example above achieves this provided you have the correct methods defined on the related models.
It would still be interesting to see an example of eager loading the whole thing.

Can I force the auto-generated Linq-to-SQL classes to use an OUTER JOIN?

Let's say I have an Order table which has a FirstSalesPersonId field and a SecondSalesPersonId field. Both of these are foreign keys that reference the SalesPerson table. For any given order, either one or two salespersons may be credited with the order. In other words, FirstSalesPersonId can never be NULL, but SecondSalesPersonId can be NULL.
When I drop my Order and SalesPerson tables onto the "Linq to SQL Classes" design surface, the class builder spots the two FK relationships from the Order table to the SalesPerson table, and so the generated Order class has a SalesPerson field and a SalesPerson1 field (which I can rename to SalesPerson1 and SalesPerson2 to avoid confusion).
Because I always want to have the salesperson data available whenever I process an order, I am using DataLoadOptions.LoadWith to specify that the two salesperson fields are populated when the order instance is populated, as follows:
dataLoadOptions.LoadWith<Order>(o => o.SalesPerson1);
dataLoadOptions.LoadWith<Order>(o => o.SalesPerson2);
The problem I'm having is that Linq to SQL is using something like the following SQL to load an order:
SELECT ...
FROM Order O
INNER JOIN SalesPerson SP1 ON SP1.salesPersonId = O.firstSalesPersonId
INNER JOIN SalesPerson SP2 ON SP2.salesPersonId = O.secondSalesPersonId
This would make sense if there were always two salesperson records, but because there is sometimes no second salesperson (secondSalesPersonId is NULL), the INNER JOIN causes the query to return no records in that case.
What I effectively want here is to change the second INNER JOIN into a LEFT OUTER JOIN. Is there a way to do that through the UI for the class generator? If not, how else can I achieve this?
(Note that because I'm using the generated classes almost exclusively, I'd rather not have something tacked on the side for this one case if I can avoid it).
Edit: per my comment reply, the SecondSalesPersonId field is nullable (in the DB, and in the generated classes).
The default behaviour actually is a LEFT JOIN, assuming you've set up the model correctly.
Here's a slightly anonymized example that I just tested on one of my own databases:
class Program
{
static void Main(string[] args)
{
using (TestDataContext context = new TestDataContext())
{
DataLoadOptions dlo = new DataLoadOptions();
dlo.LoadWith<Place>(p => p.Address);
context.LoadOptions = dlo;
var places = context.Places.Where(p => p.ID >= 100 && p.ID <= 200);
foreach (var place in places)
{
Console.WriteLine(p.ID, p.AddressID);
}
}
}
}
This is just a simple test that prints out a list of places and their address IDs. Here is the query text that appears in the profiler:
SELECT [t0].[ID], [t0].[Name], [t0].[AddressID], ...
FROM [dbo].[Places] AS [t0]
LEFT OUTER JOIN (
SELECT 1 AS [test], [t1].[AddressID],
[t1].[StreetLine1], [t1].[StreetLine2],
[t1].[City], [t1].[Region], [t1].[Country], [t1].[PostalCode]
FROM [dbo].[Addresses] AS [t1]
) AS [t2] ON [t2].[AddressID] = [t0].[AddressID]
WHERE ([t0].[PlaceID] >= #p0) AND ([t0].[PlaceID] <= #p1)
This isn't exactly a very pretty query (your guess is as good as mine as to what that 1 as [test] is all about), but it's definitively a LEFT JOIN and doesn't exhibit the problem you seem to be having. And this is just using the generated classes, I haven't made any changes.
Note that I also tested this on a dual relationship (i.e. a single Place having two Address references, one nullable, one not), and I get the exact same results. The first (non-nullable) gets turned into an INNER JOIN, and the second gets turned into a LEFT JOIN.
It has to be something in your model, like changing the nullability of the second reference. I know you say it's configured as nullable, but maybe you need to double-check? If it's definitely nullable then I suggest you post your full schema and DBML so somebody can try to reproduce the behaviour that you're seeing.
If you make the secondSalesPersonId field in the database table nullable, LINQ-to-SQL should properly construct the Association object so that the resulting SQL statement will do the LEFT OUTER JOIN.
UPDATE:
Since the field is nullable, your problem may be in explicitly declaring dataLoadOptions.LoadWith<>(). I'm running a similar situation in my current project where I have an Order, but the order goes through multiple stages. Each stage corresponds to a separate table with data related to that stage. I simply retrieve the Order, and the appropriate data follows along, if it exists. I don't use the dataLoadOptions at all, and it does what I need it to do. For example, if the Order has a purchase order record, but no invoice record, Order.PurchaseOrder will contain the purchase order data and Order.Invoice will be null. My query looks something like this:
DC.Orders.Where(a => a.Order_ID == id).SingleOrDefault();
I try not to micromanage LINQ-to-SQL...it does 95% of what I need straight out of the box.
UPDATE 2:
I found this post that discusses the use of DefaultIfEmpty() in order to populated child entities with null if they don't exist. I tried it out with LINQPad on my database and converted that example to lambda syntax (since that's what I use):
ParentTable.GroupJoin
(
ChildTable,
p => p.ParentTable_ID,
c => c.ChildTable_ID,
(p, aggregate) => new { p = p, aggregate = aggregate }
)
.SelectMany (a => a.aggregate.DefaultIfEmpty (),
(a, c) => new
{
ParentTableEntity = a.p,
ChildTableEntity = c
}
)
From what I can figure out from this statement, the GroupJoin expression relates the parent and child tables, while the SelectMany expression aggregates the related child records. The key appears to be the use of the DefaultIfEmpty, which forces the inclusion of the parent entity record even if there are no related child records. (Thanks for compelling me to dig into this further...I think I may have found some useful stuff to help with a pretty huge report I've got on my pipeline...)
UPDATE 3:
If the goal is to keep it simple, then it looks like you're going to have to reference those salesperson fields directly in your Select() expression. The reason you're having to use LoadWith<>() in the first place is because the tables are not being referenced anywhere in your query statement, so the LINQ engine won't automatically pull that information in.
As an example, given this structure:
MailingList ListCompany
=========== ===========
List_ID (PK) ListCompany_ID (PK)
ListCompany_ID (FK) FullName (string)
I want to get the name of the company associated with a particular mailing list:
MailingLists.Where(a => a.List_ID == 2).Select(a => a.ListCompany.FullName)
If that association has NOT been made, meaning that the ListCompany_ID field in the MailingList table for that record is equal to null, this is the resulting SQL generated by the LINQ engine:
SELECT [t1].[FullName]
FROM [MailingLists] AS [t0]
LEFT OUTER JOIN [ListCompanies] AS [t1] ON [t1].[ListCompany_ID] = [t0].[ListCompany_ID]
WHERE [t0].[List_ID] = #p0

Resources