One-to-zero-or-one relationship - ihp

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)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Related

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.

Write to table without primary key using Entity Framework

I have a table and I want to insert entries with entity framework. By design, the table cannot have a meaningful primary key.
CREATE TABLE dbo.ChargeCarrier_Storage
(
ID_ChargeCarrier INT NOT NULL,
ID_Storage INT NULL,
PickedUpOn DATETIME2(2) NULL,
UnloadedOn DATETIME2(2) NULL,
TransportedByDevice NVARCHAR(50) NOT NULL,
CONSTRAINT FK_ChargeCarrier_Storage_to_Storage FOREIGN KEY (ID_Storage) REFERENCES Storage(ID) ON DELETE CASCADE,
CONSTRAINT FK_ChargeCarrier_Storage_to_ChargeCarrier FOREIGN KEY (ID_ChargeCarrier) REFERENCES ChargeCarrier(ID) ON DELETE CASCADE,
CONSTRAINT CCS_OneNotNull CHECK (PickedUpOn IS NOT NULL OR UnloadedOn IS NOT NULL),
CONSTRAINT CCS_OnForklift CHECK (ID_Storage IS NULL AND PickedUpOn IS NOT NULL OR ID_Storage IS NOT NULL)
)
GO
CREATE CLUSTERED INDEX IX_ChargeCarrier_Storage ON dbo.ChargeCarrier_Storage (ID_ChargeCarrier)
GO
CREATE UNIQUE INDEX IX_OnForklift ON dbo.ChargeCarrier_Storage (ID_ChargeCarrier, ID_Storage) WHERE ID_Storage IS NULL
GO
The table contains a track list of charge carriers and storage locations. The three non-id fields contain information about which forklift moved the charge carrier, and when. The initial entry for each charge carrier, created by the system, only contains an unload date and both IDs. When a forklift picks up something, a new entry should be created with only the three fields ID_ChargeCarrier, PickedUpOn and TransportedByDevice being set. As the forklift unloads the charge carrier, the entry should be updated with the unload date and the ID of the storage location where the piece was transported to.
ID_ChargeCarrier must always be filled. For each of those IDs, there can only be one single entry with ID_Storage set to NULL, as defined by IX_OnForklift. A charge carrier can appear on the same storage multiple times.
I could make the combination of ID_ChargeCarrier, ID_Storage and PickedUpOn the primary key, but that also doesn't work, because MS-SQL doesn't allow PKs with a nullable column.
As you can see, there is no other meaningful primary key. I strictly don't want to introduce an otherwise-superfluous ID column just to make EF happy.
How can I make the insert work anyways through Entity Framework?
From the comments I see this is dealing with a legacy system.
What code would be considered the "owner" of this data, and how many other places (code, systems, reports, etc.) "touch" this data? Do you foresee needing to ever be querying against this data via EF?
If you just need to insert rows based on an event and don't care to ever query against this data via EF (at least for the foreseeable future) then I'd suggest merely inserting via a raw SQL statement and being done with it. New code for other areas may be starting to leverage EF, but "being consistent for consistency's sake" is never an argument I make. :)
The table design is poor. If this re-factoring needs to rely on this table, and the other touch-points are manageable then I would be arguing to re-design the table into something like:
ID_ChargeCarrier INT NOT NULL,
ID_Storage INT NULL,
EventTypeId INT,
EventOn DATETIME2(2) NOT NULL,
TransportedByDevice NVARCHAR(50) NOT NULL,
Where EventTypeId reflects a Pickup or DropOff and EventOn is the Date. This would accommodate a PK/unique constraint across ID_ChargeCarrier, EventTypeId, and EventOn.
Heck, throw a PK column in, and re-factor TransportedByDevice to a FK to save space as I'm guessing this table will house a significant # of records. Porting existing data into a new structure shouldn't pose any issue other than processing time.
Or at a minimum keeping the same compatible structure, appending a proper PK into the table. For example you can use:
ID_ChargeCarrier INT NOT NULL,
ID_Storage INT NULL,
PickedUpOn DATETIME2(2) NULL,
UnloadedOn DATETIME2(2) NULL,
TransportedByDevice NVARCHAR(50) NOT NULL,
ID_ChargeCarrierStorage INT IDENTITY(1,1) NOT NULL
/w a PK constraint on the new identity column. This should be able to be appended without a table re-create. However, I expect that this table could be quite large so this would reflect a rather expensive operation that should be tested and scheduled accordingly.
EF needs a key defined to determine a unique row identifier. It doesn't even need to be declared as a PK in the database, though it is still restricted to using non-nullable fields. If these are records that you will be going to throughout the life of the system I would strongly recommend using a DB structure that accommodates a legal PK. I have had tables bound to entities that did not have PKs defined, but these were strictly transient staging tables where I loaded data from an external source like Excel, wired up some entities to the table to process the data and move relevant bits along to a permanent table.
EF just needs an Entity Key. It doesn't have to map to a real database Primary Key.
And you should put an unique index on the mapped fields in the database (or risk poor performance and wierd behavior).
In SQL Server unique indexes can have nullable columns. And you can map non-nullable Entity Properties to nullable database columns.
Here's an example using that table definition:
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity;
using System.Linq;
namespace Ef6Test
{
class ChargeCarrier_Storage
{
[Key()]
[Column(Order =0)]
public int ID_ChargeCarrier { get; set; }
[Key()]
[Column(Order = 1)]
public int ID_Storage { get; set; }
[Key()]
[Column(Order = 2)]
public DateTime PickedUpOn { get; set; }
public DateTime UnloadedOn { get; set; }
public string TransportedByDevice { get; set; }
}
class Db : DbContext
{
public Db(string constr) : base(constr) { }
public DbSet<ChargeCarrier_Storage> ChargeCarrier_Storage { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
}
}
class MyDbInitializer : IDatabaseInitializer<Db>
{
public void InitializeDatabase(Db context)
{
var sql = #"
drop table if exists dbo.ChargeCarrier_Storage;
CREATE TABLE dbo.ChargeCarrier_Storage
(
ID_ChargeCarrier INT NOT NULL,
ID_Storage INT NULL,
PickedUpOn DATETIME2(2) NULL,
UnloadedOn DATETIME2(2) NULL,
TransportedByDevice NVARCHAR(50) NOT NULL,
--CONSTRAINT FK_ChargeCarrier_Storage_to_Storage FOREIGN KEY (ID_Storage) REFERENCES Storage(ID) ON DELETE CASCADE,
--CONSTRAINT FK_ChargeCarrier_Storage_to_ChargeCarrier FOREIGN KEY (ID_ChargeCarrier) REFERENCES ChargeCarrier(ID) ON DELETE CASCADE,
CONSTRAINT CCS_OneNotNull CHECK (PickedUpOn IS NOT NULL OR UnloadedOn IS NOT NULL),
CONSTRAINT CCS_OnForklift CHECK (ID_Storage IS NULL AND PickedUpOn IS NOT NULL OR ID_Storage IS NOT NULL)
)
CREATE CLUSTERED INDEX IX_ChargeCarrier_Storage ON dbo.ChargeCarrier_Storage (ID_ChargeCarrier)
CREATE UNIQUE INDEX IX_OnForklift ON dbo.ChargeCarrier_Storage (ID_ChargeCarrier, ID_Storage) WHERE ID_Storage IS NULL
";
context.Database.ExecuteSqlCommand(sql);
}
}
class Program
{
static string constr = "server=.;database=ef6test;integrated security=true";
static void Main(string[] args)
{
Database.SetInitializer<Db>( new MyDbInitializer());
using (var db = new Db(constr))
{
var f = new ChargeCarrier_Storage();
f.ID_ChargeCarrier = 2;
f.ID_Storage = 2;
f.PickedUpOn = DateTime.Now;
f.TransportedByDevice = "SomeDevice";
db.ChargeCarrier_Storage.Add(f);
db.SaveChanges();
}
using (var db = new Db(constr))
{
var c = db.ChargeCarrier_Storage.First();
}
Console.WriteLine("Hit any key to exit");
Console.ReadKey();
}
}
}
Eventually I "solved" the problem by completely re-doing the table. I removed a lot of the clutter and added a primary key.
CREATE TABLE dbo.ChargeCarrier_Storage
(
ID_ChargeCarrier INT NOT NULL,
ID_Storage INT NOT NULL,
ID_User INT NULL,
StoredOn DATETIME2(2) NOT NULL DEFAULT GetDate(),
CONSTRAINT PK_ChargeCarrier_Storage PRIMARY KEY (ID_ChargeCarrier, ID_Storage, StoredOn),
CONSTRAINT FK_ChargeCarrier_Storage_to_Storage FOREIGN KEY (ID_Storage) REFERENCES Storage(ID),
CONSTRAINT FK_ChargeCarrier_Storage_to_ChargeCarrier FOREIGN KEY (ID_ChargeCarrier) REFERENCES ChargeCarrier(ID)
)
GO
I consider a forklift to be it's own "storage", so the loading and unloading is fully abstracted away now.

EF Core conditional relationship without foreign key

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

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"`
}

How to stop redundancy

Here is my little SQL table which contains sequels of movies:
CREATE TABLE "films"
(
"title" TEXT NOT NULL,
"year" INTEGER NOT NULL,
"predecessor_title" TEXT NOT NULL,
"predecessor_year" INTEGER NOT NULL,
"increase" NUMERIC NOT NULL
)
Here are some of my values:
Toy Story 2 1999 Toy Story 1995 28%
Toy Story 3 2010 Toy Story 2 1999 69%
As you can see, it is somewhat redundant. Toy story 2 shows up both as a title and a predecessor. How can I create a table that doesn't have this?
You can:
CREATE TABLE FILMS (
ID INTEGER NOT NULL PRIMARY KEY
TITLE TEXT NOT NULL,
YEAR INTEGER NOT NULL,
PREDECESSOR_ID INTEGER,
INCREASE NUMERIC
)
ID is the primary key.
PREDECESSOR_ID is the ID of the predecessor movie. Can be null because the first movie of the series doesn't have a predecessor.
INCREASE is the increase. Can be null because the first movie doesn't have an increase.
When you design it using ORM (Hibernate) tool like this:
#Entity
public class Films{
#ID
#GeneratedValue
int id;
#NotNull
String titleText;
#NotNull
Integer year;
#JoinColumn
Film film;
Integer increase;
}
this looks cool. Here you can keep a reference of predecessor in Film film; No, matter if there is no predecessor. It would be null then.