0

Please consider the following tables and data that is inserted:

CREATE TABLE [dbo].[Language]
(
    [Id] UNIQUEIDENTIFIER NOT NULL,
    [Name] NVARCHAR(256) NOT NULL,
    [Culture] NVARCHAR(10) UNIQUE NOT NULL,
    [DateCreated] DATETIME NOT NULL DEFAULT GETUTCDATE(),
    CONSTRAINT [PK_Language] PRIMARY KEY CLUSTERED ([Id] ASC)
)

CREATE TABLE Book (
    [Id] UNIQUEIDENTIFIER NOT NULL PRIMARY KEY,
    [PublicDate] DATETIME DEFAULT GETUTCDATE()
)

CREATE TABLE [BookLanguage] (
    [BookId] UNIQUEIDENTIFIER NOT NULL,
    [LanguageId] UNIQUEIDENTIFIER NOT NULL,
    [Default] BIT NOT NULL,
    CONSTRAINT [PK_BookLanguage] PRIMARY KEY CLUSTERED ([BookId] ASC, [LanguageId] ASC),
    CONSTRAINT [FK_BookLanguage_Book] FOREIGN KEY ([BookId]) REFERENCES [Book]([Id]),
    CONSTRAINT [FK_BookLanguage_Language] FOREIGN KEY ([LanguageId]) REFERENCES [Language]([Id])
)

CREATE TABLE BookLocalization(
    [Id] INT IDENTITY(1,1) PRIMARY KEY,
    [BookId] UNIQUEIDENTIFIER NOT NULL,
    [Name] NVARCHAR(256),
    [LanguageId] UNIQUEIDENTIFIER,
    CONSTRAINT [FK_BookLocalization_Book] FOREIGN KEY ([BookId]) REFERENCES [Book]([Id])
)

INSERT INTO [Language]([Id], [Name], [Culture], [DateCreated])
VALUES ('2F12CAA6-16D7-4D83-B17E-560241DAE1D2', 'English', 'en', GETUTCDATE())

INSERT INTO [Language]([Id], [Name], [Culture], [DateCreated])
VALUES ('895E0F72-413C-48CD-A1A1-6302AC8A4CB4', 'Spanish', 'es', GETUTCDATE())

INSERT INTO [Book] ([Id], [PublicDate])
VALUES('D31A6823-5415-407F-9B49-49136242F03F', GETUTCDATE())

INSERT INTO [BookLanguage] ([BookId], [LanguageId], [Default])
VALUES ('D31A6823-5415-407F-9B49-49136242F03F', '2F12CAA6-16D7-4D83-B17E-560241DAE1D2', 1)

INSERT INTO [BookLanguage] ([BookId], [LanguageId], [Default])
VALUES ('D31A6823-5415-407F-9B49-49136242F03F', '895E0F72-413C-48CD-A1A1-6302AC8A4CB4', 0)

INSERT INTO [BookLocalization] ([BookId], [Name], [LanguageId])
VALUES ('D31A6823-5415-407F-9B49-49136242F03F', 'My First Book', '2F12CAA6-16D7-4D83-B17E-560241DAE1D2')

INSERT INTO [BookLocalization] ([BookId], [Name], [LanguageId])
VALUES ('D31A6823-5415-407F-9B49-49136242F03F', 'Mi Primer Libro', '895E0F72-413C-48CD-A1A1-6302AC8A4CB4')

I am trying to figure out how to select the book that the user wants with its localization record and if the language does not exist for selected book, then to return the default language that is indicated for that book.

For example, the book has english and spanish records. So if I pass in the english language Id then I get english. If I pass the spanish language id then I get spanish. If I pass french language id, then I get english because the book has no french language.

Here is what I tried:

SELECT B.[Id], B.[PublicDate], 
    BL.[Id] AS BookLocalizationId, BL.[Name], BL.[LanguageId],
    BL2.[LanguageId], BL2.[Default]
FROM [Book] B
    INNER JOIN [BookLocalization] BL ON BL.[BookId] = B.[Id]
    INNER JOIN [BookLanguage] BL2 ON BL2.[BookId] = B.[Id]

The problem with the above is that it returns 4 records. I don't fully understand exactly how I end up with 4 records and I am hoping somebody can explain why and tell me how I can achieve returning only 1 record. Or if one not possible, then two. 1st being the language I selected, 2nd being the default language for that table and then I can in code pick which record to return to user.

Edit: BookLanguage represents all languages available/allowed for that book. Some books are translated in 2 languages, other in 5. That table controls what languages are allowed for the book to be translated in. BookLocalization is the actual translation.

GMB
  • 216,147
  • 25
  • 84
  • 135
Bagzli
  • 6,254
  • 17
  • 80
  • 163
  • 1
    BookLanguage represents all languages available/allowed for that book. Some books are translated in 2 languages, other in 5. That table controls what languages are allowed for the book to be translated in. BookLocalization is the actual translation. – Bagzli Jul 06 '23 at 20:14
  • Agreed, I have included it in the question now. – Bagzli Jul 06 '23 at 20:21
  • just a little aside, do you really need language id as uniqueidentifier? – siggemannen Jul 06 '23 at 20:40
  • 1
    @siggemannen have to have? probably not. It does make some interactions a lot easier when going from c# to SQL and not having to rely on incremental identity. What are you thinking though, why should I avoid it? – Bagzli Jul 06 '23 at 20:41
  • To use or not to use uniqueidentifier ids is much debated :) you can read up on the pros and cons, but in general it shouldn't be an issue. – Dale K Jul 06 '23 at 20:51
  • It kinda cluttered your examples, besides having few upsides unless one needs to merge data from multiple systems with potential ID overlap. But as @DaleK says, it's no biggie. – siggemannen Jul 06 '23 at 21:13

1 Answers1

2

Your join of book language is not specific enough, you need to also join on language, else you get the cross-join effect you were experiencing.

And to get either the specific language or the default language, you can use TOP 1 with ORDER BY where you order by those 2 conditions.

declare @LanguageId uniqueidentifier = '895E0F72-413C-48CD-A1A1-6302AC8A4CB4';

select top 1 b.[Id], b.[PublicDate], 
    blz.[Id] AS BookLocalizationId, blz.[Name], blz.[LanguageId],
    blg.[LanguageId], blg.[Default]
from Book b
inner join BookLocalization blz on blz.BookId = b.id
inner join BookLanguage blg on blg.BookId = blz.BookId
    and blg.LanguageId = blz.LanguageId
order by case when @LanguageId = blz.LanguageId then 1 else 0 end desc
    , blg.[Default] desc;

DbFiddle

Joining notes: When you join 1 Book with 2 BookLocalization rows you get 2 rows (i.e. Books * BookLocalizations). When you come to join BookLanguage on, for which you have another 2 rows, if you only join with the BookId you get 4 rows, because each BookLanguage row matches both BookLocalizations rows (i.e. Books * BookLocalizations * BookLanguages). But what you actually want is to match a BookLocalization with a BookLanguage on both BookId and LanguageId.

DbFiddle

Dale K
  • 25,246
  • 15
  • 42
  • 71
  • Are you sure about joining the Language table? I ran your query without that and I got same result. I think what is eliminating the 2 extra records is the "AND clause" that you have on BookLanguage join. – Bagzli Jul 06 '23 at 20:25
  • Are you able to explain to me exactly why does the 'and blg.LanguageId = blz.LanguageId' eliminates the 2 extra rows? Sorry I am trying to understand this more than getting the correct query so that I can apply it in future coding. This was just a dumbed down example of actual code so I can isolate the problem I'm facing. I can translate to actual tables just wanting to understand a bit more as to why it works. – Bagzli Jul 06 '23 at 20:27
  • Thank you, that really does help! Wondering if you would answer one last thing for me. Promise its last. If I had the exact scenario above and I did not know the book id and was just doing a general search, would I use ROW_NUMBER and grab the first record as my results or is there a better way? – Bagzli Jul 06 '23 at 20:40
  • I don't quite understand? Your scenario doesn't make use of a bookid... but you only have 1 book record in your table. You probably want another question for this to understand what you want returned, just any one random book? Or 1 language row per book? I suggest a new question, because its quite different to this one. – Dale K Jul 06 '23 at 20:44