Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Foreign Keys with cascading deletes #1261

Merged
merged 16 commits into from
Jun 18, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
root = true

[*]
end_of_line = lf
charset = utf-8
indent_style = space
indent_size = 4
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,30 @@ In the `Issue` referencing `User` example above, this means that if you create a
`Issue` in the same session, when you call `IDocumentSession.SaveChanges()/SaveChangesAsync()`, Marten will know
to save the new user first so that the issue will not fail with referential integrity violations.

## Foreign Keys to non-Marten tables

Marten can also create a foreign key to tables that are not managed by Marten. Continuing the our sample
of `Issue`, we can create a foreign key from our `Issue` to our external bug tracking system:

<[sample:configure-external-foreign-key]>

With the configuration above, Marten will generate a foreign key constraint from the `Issue` to a table in the
`bug-tracker` schema called `bugs` on the `id` column. The constraint would be defined as:

<pre>
ALTER TABLE public.mt_doc_issue
ADD CONSTRAINT mt_doc_issue_bug_id_fkey FOREIGN KEY (bug_id)
REFERENCES bug-tracker.bugs (id);
</pre>

## Cascading deletes

Marten can also cascade deletes on the foreign keys that it creates. The `ForeignKeyDefinition` has a
`CascadeDeletes` property that indicates whether the foreign key should enable cascading deletes. One way
to enable this is to use a configuration function like:

<[sample:cascade_deletes_with_config_func]>

## Configuring with Attributes

You can optionally configure properties or fields as foreign key relationships with the `[ForeignKey]` attribute:
Expand Down
236 changes: 236 additions & 0 deletions src/Marten.Testing/Acceptance/foreign_keys.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
using System;
using Marten.Testing.Documents;
using Shouldly;
using Xunit;

namespace Marten.Testing.Acceptance
{
public class foreign_keys: IntegratedFixture
{
[Fact]
public void can_insert_document_with_null_value_of_foreign_key()
{
ConfigureForeignKeyWithCascadingDeletes(false);

var issue = new Issue();

ShouldProperlySave(issue);
}

[Fact]
public void can_insert_document_with_existing_value_of_foreign_key()
{
ConfigureForeignKeyWithCascadingDeletes(false);

var user = new User();
using (var session = theStore.OpenSession())
{
session.Store(user);
session.SaveChanges();
}

var issue = new Issue { AssigneeId = user.Id };

ShouldProperlySave(issue);
}

[Fact]
public void cannot_insert_document_with_non_existing_value_of_foreign_key()
{
ConfigureForeignKeyWithCascadingDeletes(false);

var issue = new Issue { AssigneeId = Guid.NewGuid() };

Should.Throw<MartenCommandException>(() =>
{
using (var session = theStore.OpenSession())
{
session.Insert(issue);
session.SaveChanges();
}
});
}

[Fact]
public void can_update_document_with_existing_value_of_foreign_key_to_other_existing_value()
{
ConfigureForeignKeyWithCascadingDeletes(false);

var user = new User();
var otherUser = new User();
var issue = new Issue { AssigneeId = user.Id };

using (var session = theStore.OpenSession())
{
session.Store(user, otherUser);
session.Store(issue);
session.SaveChanges();
}

issue.AssigneeId = otherUser.Id;

ShouldProperlySave(issue);
}

[Fact]
public void can_update_document_with_existing_value_of_foreign_key_to_null()
{
ConfigureForeignKeyWithCascadingDeletes(false);

var user = new User();
var otherUser = new User();
var issue = new Issue { AssigneeId = user.Id };

using (var session = theStore.OpenSession())
{
session.Store(user, otherUser);
session.Store(issue);
session.SaveChanges();
}

issue.AssigneeId = null;

ShouldProperlySave(issue);
}

[Fact]
public void cannot_update_document_with_existing_value_of_foreign_key_to_not_existing()
{
ConfigureForeignKeyWithCascadingDeletes(false);

var user = new User();
var otherUser = new User();
var issue = new Issue { AssigneeId = user.Id };

using (var session = theStore.OpenSession())
{
session.Store(user, otherUser);
session.Store(issue);
session.SaveChanges();
}

issue.AssigneeId = Guid.NewGuid();

Should.Throw<MartenCommandException>(() =>
{
using (var session = theStore.OpenSession())
{
session.Update(issue);
session.SaveChanges();
}
});
}

[Fact]
public void can_delete_document_with_foreign_key()
{
ConfigureForeignKeyWithCascadingDeletes(true);

var user = new User();
var issue = new Issue { AssigneeId = user.Id };

using (var session = theStore.OpenSession())
{
session.Store(user);
session.Store(issue);
session.SaveChanges();
}

using (var session = theStore.OpenSession())
{
session.Delete(issue);
session.SaveChanges();
}

using (var query = theStore.QuerySession())
{
query.Load<Issue>(issue.Id).ShouldBeNull();
query.Load<User>(user.Id).ShouldNotBeNull();
}
}

[Fact]
public void can_delete_document_that_is_referenced_by_foreignkey_with_cascadedeletes_from_other_document()
{
ConfigureForeignKeyWithCascadingDeletes(true);

var user = new User();
var issue = new Issue { AssigneeId = user.Id };

using (var session = theStore.OpenSession())
{
session.Store(user);
session.Store(issue);
session.SaveChanges();
}

using (var session = theStore.OpenSession())
{
session.Delete(user);
session.SaveChanges();
}

using (var query = theStore.QuerySession())
{
query.Load<Issue>(issue.Id).ShouldBeNull();
query.Load<User>(user.Id).ShouldBeNull();
}
}

[Fact]
public void cannot_delete_document_that_is_referenced_by_foreignkey_without_cascadedeletes_from_other_document()
{
ConfigureForeignKeyWithCascadingDeletes(false);

var user = new User();
var issue = new Issue { AssigneeId = user.Id };

using (var session = theStore.OpenSession())
{
session.Store(user);
session.Store(issue);
session.SaveChanges();
}

Should.Throw<MartenCommandException>(() =>
{
using (var session = theStore.OpenSession())
{
session.Delete(user);
session.SaveChanges();
}
});

using (var query = theStore.QuerySession())
{
query.Load<Issue>(issue.Id).ShouldNotBeNull();
query.Load<User>(user.Id).ShouldNotBeNull();
}
}

private void ConfigureForeignKeyWithCascadingDeletes(bool hasCascadeDeletes)
{
StoreOptions(options =>
{
options.Schema.For<Issue>().ForeignKey<User>(x => x.AssigneeId, fkd => fkd.CascadeDeletes = hasCascadeDeletes);
});
theStore.Tenancy.Default.EnsureStorageExists(typeof(User));
}

private void ShouldProperlySave(Issue issue)
{
using (var session = theStore.OpenSession())
{
session.Store(issue);
session.SaveChanges();
}

using (var query = theStore.QuerySession())
{
var documentFromDb = query.Load<Issue>(issue.Id);

documentFromDb.ShouldNotBeNull();
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@

namespace Marten.Testing.Bugs
{
public class Bug_1258_cannot_derive_updates_for_objects : IntegratedFixture
public class Bug_1258_cannot_derive_updates_for_objects: IntegratedFixture
{
[Fact]
public void can_properly_detect_changes_when_user_defined_type()
{
theStore.Advanced.Clean.CompletelyRemoveAll();
StoreOptions(_ =>
{
_.AutoCreateSchemaObjects = AutoCreate.CreateOrUpdate;
Expand Down Expand Up @@ -118,7 +119,7 @@ CREATE FUNCTION cust_type_ne(cust_type, cust_type) RETURNS boolean IMMUTABLE LAN
RIGHTARG = cust_type,
COMMUTATOR = ~>=~,
NEGATOR = ~>~
);
);
CREATE OPERATOR ~<~ (
PROCEDURE = cust_type_lt,
LEFTARG = cust_type,
Expand Down Expand Up @@ -230,4 +231,4 @@ public class IssueForUserWithCustomType

public Guid? UserId { get; set; }
}
}
}
20 changes: 20 additions & 0 deletions src/Marten.Testing/Documents/Issue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,26 @@ public Issue()
public Guid? AssigneeId { get; set; }

public Guid? ReporterId { get; set; }

public Guid? BugId { get; set; }
}
// ENDSAMPLE

// SAMPLE: Bug
public class Bug
{
public Bug()
{
Id = Guid.NewGuid();
}

public Guid Id { get; set; }

public string Type { get; set; }

public string Title { get; set; }

public int IssueTrackerId { get; set; }
}
// ENDSAMPLE
}
45 changes: 36 additions & 9 deletions src/Marten.Testing/Examples/ForeignKeyExamples.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System;
using Marten.Testing.Documents;
using Marten.Testing.Documents;

namespace Marten.Testing.Examples
{
Expand All @@ -8,19 +7,47 @@ public class ForeignKeyExamples
public void configuration()
{
// SAMPLE: configure-foreign-key
var store = DocumentStore.For(_ =>
{
_.Connection("some database connection");
var store = DocumentStore
.For(_ =>
{
_.Connection("some database connection");

// In the following line of code, I'm setting
// up a foreign key relationship to the User document
_.Schema.For<Issue>().ForeignKey<User>(x => x.AssigneeId);
});
// In the following line of code, I'm setting
// up a foreign key relationship to the User document
_.Schema.For<Issue>().ForeignKey<User>(x => x.AssigneeId);
});
// ENDSAMPLE

//var sql = store.Schema.ToDDL();
//Console.WriteLine(sql);
}

public void external_fkey()
{
// SAMPLE: configure-external-foreign-key
var store = DocumentStore
.For(_ =>
{
_.Connection("some database connection");

// Here we create a foreign key to table that is not
// created or managed by marten
_.Schema.For<Issue>().ForeignKey(i => i.BugId, "bugtracker", "bugs", "id");
});
// ENDSAMPLE
}

public void cascade_deletes_with_config_func()
{
// SAMPLE: cascade_deletes_with_config_func
var store = DocumentStore
.For(_ =>
{
_.Connection("some database connection");

_.Schema.For<Issue>().ForeignKey<User>(x => x.AssigneeId, fkd => fkd.CascadeDeletes = true);
});
// ENDSAMPLE
}
}
}
Loading