EF Core conditional relationship without foreign key - sql

I have a table that has generic ObjectType (string) and ObjectId (int) columns that correspond to other tables and their primary keys respectively. What is the best way to set up these condition relationships with EF Core? (One to many relationship). Note: Normally I would just create properties on the C# data models that query the database for these conditions, but my data models are just POCOs, they have no access to the database.
SQL Server
Table Communication {
Id int,
ObjectType string,
ObjectId int
}
Table Cat {
Id int,
...
}
Table Dog {
Id int,
...
}
C# Models
class Communication {
int Id
string objectType
int objectId
Cat cat -> populated if objectType = "Cat"
Dog dog -> populated if objectType = "Dog"
}
class Cat {
int Id,
...
List<Communication> communications -> all associated Communications
}
class Dog {
int Id,
...
List<Communication> communications -> all associated Communications
}
Example Communication database records
Id ObjectType ObjectId
1 Cat 55
2 Cat 78
3 Dog 13

Related

Using junction tables created in schema within Spring Boot JDBC

I'm trying out pre-defining my database structure using SQL schema and then utilising it within my Kotlin code. I'm using Spring Boot and Spring Data JDBC.
Here's what I currently have so far:
My User class:
data class User(
#Column(name = SqlQueries.Users.Entries.id) // "user_id"
val id: String?,
#Column(name = SqlQueries.Users.Entries.firstName) // "user_first_name"
var firstName: String = "Joe",
#Column(name = SqlQueries.Users.Entries.lastName) // "user_last_name"
var lastName: String = "Bloggs",
#Column(name = SqlQueries.Users.Entries.username) // "user_username"
var username: String = "${firstName}.${lastName}",
#Column(name = SqlQueries.Users.Entries.password) // "user_password"
#JsonIgnore
var password: String = "password1",
#Column(name = SqlQueries.Users.Entries.isActive) // "user_is_active"
val isActive: Boolean = true,
)
My UserRole class:
data class UserRole(
#Column(name = SqlQueries.Lookups.UserRoles.Entries.id)
val id: Int? = null,
#Column(name = SqlQueries.Lookups.UserRoles.Entries.roleName)
val name: String = "",
)
My Schema:
-- ===================================================================================
-- Lookup Tables
-- ===================================================================================
-- Creates our User Table if one does not exist within the database already.
CREATE TABLE IF NOT EXISTS table_users(
user_id VARCHAR(60) DEFAULT RANDOM_UUID() UNIQUE PRIMARY KEY,
user_first_name VARCHAR NOT NULL,
user_last_name VARCHAR NOT NULL,
user_username VARCHAR NOT NULL UNIQUE,
user_password VARCHAR NOT NULL,
user_is_active VARCHAR NOT NULL
);
-- ===================================================================================
-- Lookup Tables
-- ===================================================================================
-- Creates our Roles Lookup Table if one does not already exist within the database.
CREATE TABLE IF NOT EXISTS lookup_roles(
role_id SMALLINT AUTO_INCREMENT UNIQUE PRIMARY KEY,
role_name VARCHAR NOT NULL UNIQUE
);
-- ===================================================================================
-- Junction Tables
-- ===================================================================================
-- Creates our User/Role Junction table if one does not already exist within the database.
-- This will be the foundation of a many to many relationship between the two entities.
CREATE TABLE IF NOT EXISTS junction_user_role(
user_id VARCHAR(60),
role_id SMALLINT,
CONSTRAINT pk_user_role PRIMARY KEY (user_id, role_id),
CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES table_users (user_id),
CONSTRAINT fk_role FOREIGN KEY (role_id) REFERENCES lookup_roles (role_id)
);
As you can see, I've created a User, Role and User/Role table. This is designed to have a many to many relationship.
I'm in the dark with being able to "access" the relationship as a variable which I can use later on.
In my previous "prototype" which had a different design concept I used the following format:
User class:
...
#ManyToMany(fetch = FetchType.EAGER)
#Column(name = "user_roles")
var userRoles: MutableSet<Role> = mutableSetOf(),
...
How would I define and implement this with the new way of doing it?
I don't really speak Kotlin, but I try anyway. Please use ChatGPT or personal experience to fix Kotlin errors ;-)
The first step is to identify the aggregates an what belongs to which aggregate.
I'd suggest that you have a UserRole and a User aggregate, with the later owning the relationship.
This implies that you need to add the relationship to the User, which is your aggregate root for the User aggregate.
Since Role is a different aggregate you'd reference it by id, and you need a separate entity to hold that id
data class User (
...
#MappedCollection(idColumn="user_id")
val roles: Set<RoleRef>
)
data class RoleRef(
val roleId: AggregateReference<Role, Int>
)
If you want to navigate from UserRole to all the User entities having that role, you'd create a repository method in the UserRepository for this.
How this works is detailed in https://spring.io/blog/2018/09/24/spring-data-jdbc-references-and-aggregates
Also related: Spring Data JDBC many to many relationship management

How to query parent & child table in one query?

First, I'll address concerns about duplicates:
How to query a parent table and inherited child table together in one query
This question is similar but it doesn't provide a concrete example
How can you represent inheritance in a database? suggests "Class Table Inheritance", which is the pattern I'm using, but does not explain how to query it effectively.
Here's a example of the problem I'm facing:
table Document {
id: Id
name: string
type: ??
}
table FooDoc {
id: Id
// Foreign key to Document
docId: Id
qux: string
}
table BarDoc {
id: Id
// Foreign key to document
docId: Id
baz: number
}
Ideally, I'd like to make it so that in 1 query, I can
grab a document based on its id
grab the relevant data from the correct child table
Is this possible?
There are six ways (afaik) to model table inheritance in relational databases. You chose the Permissive Class Table Inheritance option.
Now, you can use two left joins to retrieve information for child tables. The resulting columns from the non-matching type will be null.
For example:
select d.*, f.qux, b.baz
from document d
left join foodoc f on f.id = d.id
left join bardoc b on b.id = d.id
Result:
id name type qux baz
--- ----- ----- -------- ----
20 baz1 2 null 1240
10 foo1 1 content null
See running example at DB Fiddle. As you can see, column qux is null for type 2 and column baz is null for type 1.
The sample structure for this example is shown below:
create table document (
id int primary key not null,
name varchar(10),
type int not null check (type in (1, 2))
);
insert into document (id, name, type) values
(10, 'foo1', 1),
(20, 'baz1', 2);
create table foodoc (
id int primary key not null references document(id),
qux varchar(10)
);
insert into foodoc (id, qux) values (1, 'content');
create table bardoc (
id int primary key not null references document(id),
baz int
);
insert into bardoc (id, baz) values (2, 1240);
Note: Also please consider that to fully implement integrity you would need to include the type column in both foreign keys.

One-to-zero-or-one relationship

Introduction
In order to test out IHP, I've converted part of the Contoso University tutorial for ASP.NET Core to IHP.
This step in the tutorial shows a data model diagram. The part that I'll focus on in this question involves Instructor and OfficeAssignment which have a one-to-zero-or-one relationship, according to that page.
Instructor
The model in C# for Instructor is:
public class Instructor
{
public int ID { get; set; }
[Required]
[Display(Name = "Last Name")]
[StringLength(50)]
public string LastName { get; set; }
[Required]
[Column("FirstName")]
[Display(Name = "First Name")]
[StringLength(50)]
public string FirstMidName { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
[Display(Name = "Hire Date")]
public DateTime HireDate { get; set; }
[Display(Name = "Full Name")]
public string FullName
{
get { return LastName + ", " + FirstMidName; }
}
public ICollection<Course> Courses { get; set; }
public OfficeAssignment OfficeAssignment { get; set; }
}
This resulted in the following table in sqlite:
CREATE TABLE IF NOT EXISTS "Instructor" (
"ID" INTEGER NOT NULL CONSTRAINT "PK_Instructor" PRIMARY KEY AUTOINCREMENT,
"LastName" TEXT NOT NULL,
"FirstName" TEXT NOT NULL,
"HireDate" TEXT NOT NULL
);
So in IHP, I used the following:
CREATE TABLE instructors (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
last_name TEXT NOT NULL,
first_mid_name TEXT NOT NULL,
hire_date DATE NOT NULL
);
OfficeAssignment
The model in C# for OfficeAssignment is:
public class OfficeAssignment
{
[Key]
public int InstructorID { get; set; }
[StringLength(50)]
[Display(Name = "Office Location")]
public string Location { get; set; }
public Instructor Instructor { get; set; }
}
This resulted in the following table in sqlite:
CREATE TABLE IF NOT EXISTS "OfficeAssignment" (
"InstructorID" INTEGER NOT NULL CONSTRAINT "PK_OfficeAssignment" PRIMARY KEY,
"Location" TEXT NULL,
CONSTRAINT "FK_OfficeAssignment_Instructor_InstructorID" FOREIGN KEY ("InstructorID") REFERENCES "Instructor" ("ID") ON DELETE CASCADE
);
So in IHP, I used the following:
CREATE TABLE office_assignments (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
instructor_id UUID NOT NULL,
"location" TEXT NOT NULL
);
CREATE INDEX office_assignments_instructor_id_index ON office_assignments (instructor_id);
ALTER TABLE office_assignments ADD CONSTRAINT office_assignments_ref_instructor_id FOREIGN KEY (instructor_id) REFERENCES instructors (id) ON DELETE NO ACTION;
Difference in columns
Note that the ASP.NET Core version of the table for OfficeAssignment only has the following columns:
InstructorID
Location
whereas, the IHP table has:
id
instructor_id
location
I.e. it has an id column. I left it in there because it's added by default by the IHP schema editor.
One-to-zero-or-one relationship
Given the generated code for Instructor in Types.hs:
data Instructor' officeAssignments = Instructor {
id :: (Id' "instructors"),
lastName :: Text,
firstMidName :: Text,
hireDate :: Data.Time.Calendar.Day,
officeAssignments :: officeAssignments,
meta :: MetaBag
} deriving (Eq, Show)
it seems like IHP is interpreting things such that:
One instructor can have many office assignments
(I.e. the field officeAssignments is plural.)
However, according to the diagram in the ASP.NET Core tutorial, an instructor can have 0 or 1 office assignments. (I.e. they have an office or not.)
It seems that Entity Framework Core gets the signal that there should be at most one office assignment per instructor from the presence of the following navigation property on Instructor:
public OfficeAssignment OfficeAssignment { get; set; }
UPDATE: this has been confirmed. See section below titled Update 1.
Desired semantics - create instructor with an office
In the C# app, let's say I create an Instructor, specifying an office:
We see the following in sqlite:
sqlite> SELECT * FROM Instructor; SELECT * FROM OfficeAssignment;
ID LastName FirstName HireDate
-- ----------- --------- -------------------
1 Fakhouri Fadi 2002-07-06 00:00:00
2 Harui Roger 1998-07-01 00:00:00
3 Kapoor Candace 2001-01-15 00:00:00
4 Zheng Roger 2004-02-12 00:00:00
5 Abercrombie Kim 1995-03-11 00:00:00
10 Curry Haskell 1920-01-01 00:00:00
InstructorID Location
------------ ------------
2 Gowan 27
3 Thompson 304
10 Haskell Room
Desired semantics - create instructor without an office
Now, in the C# app, let's create an instructor and not specify an office:
We see the following in sqlite:
sqlite> SELECT * FROM Instructor; SELECT * FROM OfficeAssignment;
ID LastName FirstName HireDate
-- ----------- --------- -------------------
1 Fakhouri Fadi 2002-07-06 00:00:00
2 Harui Roger 1998-07-01 00:00:00
3 Kapoor Candace 2001-01-15 00:00:00
4 Zheng Roger 2004-02-12 00:00:00
5 Abercrombie Kim 1995-03-11 00:00:00
10 Curry Haskell 1920-01-01 00:00:00
11 Church Alonzo 1940-01-01 00:00:00
InstructorID Location
------------ ------------
2 Gowan 27
3 Thompson 304
10 Haskell Room
11
Interestingly, if I edit an instructor and leave the office blank:
we see the following in sqlite:
sqlite> SELECT * FROM Instructor; SELECT * FROM OfficeAssignment;
ID LastName FirstName HireDate
-- ----------- --------- -------------------
1 Fakhouri Fadi 2002-07-06 00:00:00
2 Harui Roger 1998-07-01 00:00:00
3 Kapoor Candace 2001-01-15 00:00:00
4 Zheng Roger 2004-02-12 00:00:00
5 Abercrombie Kim 1995-03-11 00:00:00
10 Curry Haskell 1920-01-01 00:00:00
11 Church Alonzo 1940-01-01 00:00:00
InstructorID Location
------------ ------------
2 Gowan 27
3 Thompson 304
I.e. the OfficeAssignment is removed.
This code is what implements that:
if (String.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment?.Location))
instructorToUpdate.OfficeAssignment = null;
Question
Is this a good way to set things up on the IHP side to model the C# app? Or is there something I should change on the IHP side to more closely model this one-to-zero-or-one relationship?
I looked through the Relationships section of the IHP manual, but didn't notice anything regarding this sort of one-to-zero-or-one relationship. Would just like to make sure I have the models setup correctly before I venture off into the forms side of things.
Project repository
In case it's helpful, the project repository with the above is at:
https://github.com/dharmatech/ContosoUniversityIhp/tree/2021-09-04-02-queryOr-fix
(It's very messy as it's for experimentation.)
Note
I realize this is a complex question but my hope is that it can serve as an example for folks setting up a similar relationship scenario in IHP in the future.
Update 1
The Entity Framework Core documentation has the following section:
One-to-one
It mentions:
One to one relationships have a reference navigation property on both sides. They follow the same conventions as one-to-many relationships, but a unique index is introduced on the foreign key property to ensure only one dependent is related to each principal.
So that is indeed what we see in the C# models for Instructor and OfficeAssignment. So I guess the question is, does IHP explicitly support this sort of relationship? And if not, what's a good way to simulate it given the current mechanisms.
Possible model for Instructor
It seems like in order for Instructor to model the fact that they can have one or zero offices, the generated model should have a field that's something like this:
officeAssignment :: Maybe OfficeAssignment
as mentioned earlier, it's currently as follows:
data Instructor' officeAssignments = Instructor {
id :: (Id' "instructors"),
lastName :: Text,
firstMidName :: Text,
hireDate :: Data.Time.Calendar.Day,
officeAssignments :: officeAssignments,
meta :: MetaBag
} deriving (Eq, Show)
Update 2
If we look at office_assignments table on the IHP side:
CREATE TABLE office_assignments (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
instructor_id UUID NOT NULL,
"location" TEXT NOT NULL
);
it's clear that since there's an id column, we can have any number of office_assignment rows for a given instructor_id.
However, if we look at the C# version:
CREATE TABLE IF NOT EXISTS "OfficeAssignment" (
"InstructorID" INTEGER NOT NULL CONSTRAINT "PK_OfficeAssignment" PRIMARY KEY,
"Location" TEXT NULL,
CONSTRAINT "FK_OfficeAssignment_Instructor_InstructorID" FOREIGN KEY ("InstructorID") REFERENCES "Instructor" ("ID") ON DELETE CASCADE
);
we note:
There is no id column.
There is only an InstructorID column.
InstructorID is the PRIMARY KEY
Thus, this seems to enforce the fact that there can only be one row in OfficeAssignments for any given Instructor.
So, perhaps it's as simple as changing the IHP schema to:
CREATE TABLE office_assignments (
instructor_id UUID PRIMARY KEY NOT NULL,
"location" TEXT NOT NULL
);
CREATE INDEX office_assignments_instructor_id_index ON office_assignments (instructor_id);
ALTER TABLE office_assignments ADD CONSTRAINT office_assignments_ref_instructor_id FOREIGN KEY (instructor_id) REFERENCES instructors (id) ON DELETE NO ACTION;
Result
OK, using the schema editor I updated office_assignments such that it now looks like this:
CREATE TABLE office_assignments (
instructor_id UUID PRIMARY KEY NOT NULL,
"location" TEXT NOT NULL
);
Here's the result during compilation:
[ 4 of 23] Compiling Generated.Types ( build/Generated/Types.hs, interpreted )
build/Generated/Types.hs:133:71: error:
• Couldn't match type ‘"instructors"’ with ‘"office_assignments"’
arising from a use of ‘QueryBuilder.filterWhere’
• In the fifth argument of ‘Instructor’, namely
‘(QueryBuilder.filterWhere
(#instructorId, id) (QueryBuilder.query #OfficeAssignment))’
In the expression:
Instructor
id lastName firstMidName hireDate
(QueryBuilder.filterWhere
(#instructorId, id) (QueryBuilder.query #OfficeAssignment))
def {originalDatabaseRecord = Just (Data.Dynamic.toDyn theRecord)}
In an equation for ‘theRecord’:
theRecord
= Instructor
id lastName firstMidName hireDate
(QueryBuilder.filterWhere
(#instructorId, id) (QueryBuilder.query #OfficeAssignment))
def {originalDatabaseRecord = Just (Data.Dynamic.toDyn theRecord)}
|
133 | let theRecord = Instructor id lastName firstMidName hireDate (QueryBuilder.filterWhere (#instructorId, id) (QueryBuilder.query #OfficeAssignment)) def { originalDatabaseRecord = Just (Data.Dynamic.toDyn theRecord) }
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Failed, three modules loaded.
build/Generated/Types.hs:234:20: error:
• Couldn't match type ‘OfficeAssignment' instructorId0
-> instructorId0’
with ‘Id' "office_assignments"’
arising from a use of ‘QueryBuilder.filterWhere’
• In the second argument of ‘(|>)’, namely
‘QueryBuilder.filterWhere (#instructorId, instructorId)’
In the expression:
builder |> QueryBuilder.filterWhere (#instructorId, instructorId)
In an equation for ‘QueryBuilder.filterWhereId’:
QueryBuilder.filterWhereId instructor_id builder
= builder |> QueryBuilder.filterWhere (#instructorId, instructorId)
|
234 | builder |> QueryBuilder.filterWhere (#instructorId, instructorId)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

go-gorm how to express many2many with additional columns

I want to express the following tables in GORM:
CREATE TABLE indexes (
id INTEGER PRIMARY KEY,
name VARCHAR
)
CREATE TABLE services (
id INTEGER PRIMARY KEY,
name VARCHAR
)
CREATE TABLE index_service (
index_id INTEGER REFERENCES indexes(id),
service_id INTEGER REFERENCES services(id),
write_active INTEGER,
PRIMARY KEY (index_id, service_id)
)
After reading through documentations and questions on stack overflow. I still cannot find an answer on how to express the additional column write_active in GORM's DSL
What I got so far is
type Index struct {
ID unit `json:"id" gorm:"primary_key"`
Name string `json:"name" gorm:"not null"`
}
type Service struct {
ID unit `json:"id" gorm:"primary_key"`
Name string `json:"name" gorm:"not null"`
}
However, I do not know how to write the composite table.
you need to create extra model like this:
package database
type IndexService struct {
WriteActive bool `gorm:"not null,DEFAULT false"`
}

NHibernate Set Property Default Value

Is it possible to set the default value of a property in NHibernate? Here is the scenario:
I have a self join table Category. The class and table structure are as follows:
Category
int Id
string Name
Category ParentCategory
Category
int Id not null
varchar Name not null
int ParentCategoryId not null
If a category has no parent, the ParentCategoryId must be 0.
How do I do this? TIA.
If nHibernate is enforcing this relationship I don't believe you can make it form an invalid relationship. I'd create a dummy parent record 0 in the database and just assign everything to that.
If the ParentCategoryId is not constrained by a foreign key, then you can have in your code ... something like :
class Category{
....
public static Category NoParent{
get{ return new Category{Id = 0}; }
}
....
}
and now, instead of setting to null, just set it to NoParent. Or in the setter of ParentCategory, if value is null, then set it to NoParent.
This is basically Spencer Ruport's ideea :P