Return additional relationship data in Laravel Pivot table (Laravel 9, Inertia, Vue) - eloquent

I'm in a Laravel 9 project trying to access extra relationship data in a pivot table - I'm sure this will have been answered before but the answers are confusing me more!
I have a set of courses, a set of cohorts and a set of sessions. Each course can be run multiple times (each one of these I've called an instance), with multiple sessions, and one cohort.
I've created an instances table in my database which has:
course_id
cohort_id
I have a sessions table which has data on each individual session, for example a name, a review date and review status.
I then have a pivot table called instance_sessions which looks like this:
instance_id
session_id
date
trainer_id
zoom_room_id
cohort_id
I'm not sure I need the cohort id but thats by the by :-)
My question is how can I get the relationships from this pivot table to the trainer and the zoom room?
In my instance model my relationships are setup like this:
public function sessions(){
return $this->belongsToMany(Session::class)->withPivot(['date'])
->using(InstanceSession::class);
}
public function course(){
return $this->belongsTo(Course::class);
}
public function cohort(){
return $this->hasOne(Cohort::class, 'id', 'cohort_id');
}
Session model:
function instances(){
return $this->belongsToMany(Instance::class);
}
Instance Session Model:
class InstanceSession extends Pivot
{
use HasFactory;
protected $table = 'instance_session';
protected $fillable = ['instance_id', 'session_id', 'date', 'trainer_id', 'zoom_room_id', 'cohort_id' ];
public function instance(){
return $this->belongsTo(Instance::class);
}
public function session(){
return $this->belongsToMany(Session::class);
}
public function zoomRoom(){
return $this->belongsTo(ZoomRoom::class);
}
public function trainer(){
return $this->hasOne(Trainer::class);
}
}
And my Instance Controller:
$instances = Instance::with(['course', 'cohort', 'sessions'])->get();
which returns the data to an Inertia/Vue view. I've attached an image of the data being retrieved.
Any help would be gratefully received as I'm clearly not understanding something :-)
EDIT: End goal being that I want to show on the front end all of the instances with all the sessions that belong to that instance, the date those sessions take place on, the trainer name (from trainers table) taking the session and the zoom room link (from zoom room table) for the session.

Related

How to explicitly set the ID property on an autoincrementing table in EFCore

I have a model which has an auto-incrementing ID field by default as is normal. However, I wish to seed the database with initial data and because there are foreign keys I wish to explicitly set the IDs of the seeded data.
My model
public class EntAttribute
{
public int ID { get; set; }
public string Title { get; set; }
}
My seeding code:
public class Seeder
{
private class AllAttributes
{
public List<EntAttribute> Attributes { get; set; }
}
public bool SeedData()
{
AllAttributes seedAttributes;
string strSource;
JsonSerializer JsonSer = new JsonSerializer();
strSource = System.IO.File.ReadAllText(#"Data/SeedData/Attributes.json");
seedAttributes = JsonConvert.DeserializeObject<AllAttributes>(strSource);
_context.AddRange(seedAttributes.Attributes);
_context.SaveChanges();
return true;
}
}
Please note, I'm very new to both EFCore and C#. The above is what I've managed to cobble together and it seems to work right up until I save the changes. At this point I get:
SqlException: Cannot insert explicit value for identity column in table 'Attribute' when IDENTITY_INSERT is set to OFF.
Now I'm smart enough to know that this is because I can't explicitly set the ID field in the EntAttribute table because it wants to assign its own via auto-increment. But I'm not smart enough to know what to do about it.
Any help appreciated.
EDIT: Adding the solution based on the accepted answer below because the actual code might help others...
So I added to my Context class the following:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasSequence<int>("EntAttributeNumbering")
.StartsAt(10);
modelBuilder.Entity<EntAttribute>()
.Property(i => i.ID)
.HasDefaultValueSql("NEXT VALUE FOR EntAttributeNumbering");
}
This first ensures the a sequence is created (the name is arbitrary) and then secondly, sets it to be used for the relevant table instead of auto-increment. Once this was done I was able to my seed data. There are fewer than 10 records so I only needed to set the start value for the sequence to 10. More would normally make sense but I know there will never be more.
I also had to blitz my migrations because they'd somehow got in a mess but that's probably unrelated.
With EF Core you can create and use a Sequence object to assign the IDs, and you can reserve a range of IDs for manual assignment by picking where the sequence starts. With a Sequence you can assign the IDs yourself, or let the database do it for you.
FYI for people using EF Core 3, if using int for your key you can set the start sequence value incase you have seeded data. I found this a much cleaner to solve this problem in my use case which just had a single seeded record.
e.g
modelBuilder.Entity<TableA>()
.Property(p => p.TableAId)
.HasIdentityOptions(startValue: 2);
modelBuilder.Entity<TableA>()
.HasData(
new TableA
{
TableAId = 1,
Data = "something"
});
https://github.com/npgsql/efcore.pg/issues/367#issuecomment-602111259

Laravel Eloquent Subquery Using 3 Tables

The goal of this query is to find all Stories which have an Image Type with a name of "email_small" associated with them. Not all stories have images.
There are three tables/models involved: Storys, StoryImage, and ImageTypes
A story can have many story images
/* Story model */
public function storyImages()
{
return $this->hasMany(StoryImage::class);
}
While a story image has one image type
/* Storyimage model */
public function imgtype()
{
return $this->belongsTo('App\Imagetype','imagetype_id');
}
I can get to the second step of selecting all stories with an image, but I'm lost within the subquery
$stories = Story::whereHas('storyImages', function($query){
$query->where($query->imgtype()->name, '=', 'email_small');
})
->->get();
Why can't you use two queries? First select ID of imgType with name "email_small" and then check that ID:
$id = Imagetype::where('name', 'email_small')->first()->id;
$stories = Story::whereHas('storyImages', function($q) use ($id) {
$q->where('imagetype_id', $id);
})->get();
I'm sure that even if you write everything in one function Laravel will still call few queries.

Joining datasources by name in AX 2012 form

I'm trying to join multiple datasources in a grid I created.
The grid is in CaseDetail form and it uses same tables as some other groups on it.
Therefore I have to use joins based on datasources names and not on tables names.
There is InventTable(InventTableComplaints) - parent and EcoResProductTranslation(EcoResProductTranslationComplaints) - child.
What I'm trying to do is to add this code to the child datasource:
public void executeQuery()
{
QueryBuildDataSource qbdsIT, qbdsERPTC;
qbdsIT = InventTableComplaint_DS.queryBuildDataSource();
qbdsERPTC = qbdsIT.addDataSource(tableNum(EcoResProductTranslation), "EcoResProductTranslationComplaint");
qbdsERPTC.addLink(fieldNum(InventTable, Product), fieldNum(EcoResProductTranslation, Product));
qbdsERPTC.joinMode( JoinMode::OuterJoin );
qbdsERPTC.fetchMode( QueryFetchMode::One2One );
super();
}
But it doesn't work.
Is it possible?
You do not define the table relations in the executeQuery method, do so in the init method instead which is executed exactly once. If you defined the datasource in the form (using InventTableComplaint as JoinSource and with OuterJoin as JoinMode), you do not need to do it in init method either, but you may need to define the link if not provided as table relations:
public void init()
{
qbdsIT = InventTableComplaint_DS.queryBuildDataSource();
qbdsERPTC = EcoResProductTranslationComplaint_ds.queryBuildDataSource();
qbdsERPTC.clearLinks();
qbdsERPTC.addLink(fieldNum(InventTable, Product), fieldNum(EcoResProductTranslation, Product));
super();
}
Beware that more than one record of may exist of EcoResProductTranslation for each InventTable (on for each language), so you may end out with "duplicates" of InventTable in the grid.

Paging and sorting Entity Framework on a field from Partial Class

I have a GridView which needs to page and sort data which comes from a collection of Customer objects.
Unfortunately my customer information is stored separately...the customer information is stored as a Customer ID in my database, and the Customer Name in a separate DLL.
I retrieve the ID from the database using Entity Framework, and the name from the external DLL through a partial class.
I am getting the ID from my database as follows:
public class DAL
{
public IEnumberable<Customer> GetCustomers()
{
Entities entities = new Entities();
var customers = (from c in entities.Customers
select c);
//CustomerID is a field in the Customer table
return customers;
}
}
I have then created a partial class, which retrieves the data from the DLL:
public partial class Customer
{
private string name;
public string Name
{
if (name==null)
{
DLLManager manager = new DLLManager();
name= manager.GetName(CustomerID);
}
return name;
}
}
In my business layer I can then call something like:
public class BLL
{
public List<Customer> GetCustomers()
{
DAL customersDAL = new DAL();
var customers = customersDAL.GetCustomers();
return customers.ToList();
}
}
...and this gives me a collection of Customers with ID and Name.
My problem is that I wish to page and sort by Customer Name, which as we have seen, is populated from a DLL. This means I cannot page and sort in the database, which is my preferred solution. I am therefore assuming I am going to have to call of the database records into memory, and perform paging and sorting at this level.
My question is - what is the best way to page and sort an in-memory collection. Can I do this with my List in the BLL above? I assume the List would then need to be stored in Session.
I am interested in people's thoughts on the best way to page and sort a field that does not come from the database in an Entity Framework scenario.
Very grateful for any help!
Mart
p.s. This question is a development of this post here:
GridView sorting and paging Entity Framework with calculated field
The only difference here is that I am now using a partial class, and hopefully this post is a little clearer.
Yes, you can page and sort within you list in the BLL. As long as its fast enough I wouldn't care to much about caching something in the session. An other way would be to extend your database with the data from you DLL.
I posted this question slightly differently on a different forum, and got the following solution.
Basically I return the data as an IQueryable from the DAL which has already been forced to execute using ToList(). This means that I am running my sorting and paging against an object which consists of data from the DB and DLL. This also allows Scott's dynamic sorting to take place.
The BLL then performs OrderBy(), Skip() and Take() on the returned IQueryable and then returns this as a List to my GridView.
It works fine, but I am slightly bemused that we are perfoming IQueryable to List to IQueryable to List again.
1) Get the results from the database as an IQueryable:
public class DAL
{
public IQueryable<Customer> GetCustomers()
{
Entities entities = new Entities();
var customers = (from c in entities.Customers
select c);
//CustomerID is a field in the Customer table
return customers.ToList().AsQueryable();
}
}
2) Pull the results into my business layer:
public class BLL
{
public List<Customer> GetCustomers(intint startRowIndex, int maximumRows, string sortParameter)
{
DAL customersDAL = new DAL();
return customersDAL.GetCustomers().OrderBy(sortParameter).Skip(startRowIndex).Take(maximumRows).ToList();
}
}
Here is the link to the other thread.
http://forums.asp.net/p/1976270/5655727.aspx?Paging+and+sorting+Entity+Framework+on+a+field+from+Partial+Class
Hope this helps others!

Custom Initialization Strategy for EF Code First that doesn't drop tables to add a column

The latest EF Code First NuGet package comes with a custom implementation of IDatabaseInitializer called DontDropDbJustCreateTablesIfModelChanged. As the name implies, when a model change is detected, it will not drop and recreate the whole database, just drop and recreate the tables.
Say I have this model class:
public class User
{
public string Username { get; set; }
// This property is new; the model has changed!
public string OpenID { get; set; }
}
How would one go about implementing an IDatabaseInitializer that doesn't drop any tables either. In this case, it would just add an OpenID column to the User table?
I think it is a matter of SQL. So for SQL Server you can write something like:
public class MyInitializer : IDatabaseInitializer<MyContext>
{
public void InitializeDatabase(MyContext context)
{
context.Database.SqlCommand(
#"
IF NOT EXISTS (SELECT 1 FROM sys.columns AS col
INNER JOIN sys.tables AS tab ON tab.object_Id = col.object_Id
WHERE tab.Name = 'User' AND col.Name = 'OpenId')
BEGIN
ALTER TABLE dbo.User ADD OpenId INT;
END");
}
}
But in the same way you can execute such script without adding it to your application which I think is much better approach.
With the current version of Code First, you cannot simply amend your schema and preserve any data that you might have in your tables. If maintaining data, such as reference data / lookup tables is important with this release you can create your own Initializer and override the Seed method to populate your tables
public class MyDbInitializer : DropCreateDatabaseIfModelChanges<MyContext>
{
protected override void Seed(MyContext context)
{
var countries = new List<Country>
{
new Country {Id=1, Name="United Kingdom"},
new Country{Id=2, Name="Ireland"}
};
countries.ForEach(c => context.Countries.Add(c));
}
}
And then use this in your Application_Start:
Database.SetInitializer<MyContext>(new MyDbInitializer());
I believe that this is being addressed currently by the EF Team, but wasn't ready for release at the time the Code First drop came out. You can see a preview here: Code First Migrations