-1

I have an application that generates student ID based on year, like the first ID: 202100001, then 202100002 and so on. Basically takes the year and start incrementing the counter.

That's good and easy, but my problem because it taking the last ID from database to generate the student ID. When there is a new year, the year part changes but it doesn't reset to zero. I would like in 2022 to start at: 202200001, then 202200002 and so on. Is there an easier way to reset the counter, should I add a new column for this?

This is my current code:

//Generate Student Number
string StudentNumber;
private void GenerateStudentNumber()
{
    DateTime moment = DateTime.Now;
    string year = moment.Year.ToString();
    try
    {
        StudentNumber = GenerateID();               
        txtStudentNumber.Text = year + StudentNumber;
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
    }
}

private string GenerateID()
{    
    string value = "00";
    int IDindex = 0;
    try
    {        
        using (con = new SqlConnection(databaseConnectionString))
        {
            // Fetch the latest ID from the database
            con.Open();
            cmd = new SqlCommand("SELECT TOP 1 StudentID FROM Students order BY StudentID DESC", con);
            rdr = cmd.ExecuteReader(CommandBehavior.CloseConnection);
            if (rdr.HasRows)
            {
                rdr.Read();
                IDindex = Convert.ToInt16(rdr["StudentID"]);
            }
            rdr.Close();
        }
            
        IDindex++;
        // Because incrementing a string with an integer removes 0's
        // we need to replace them. If necessary.
        if (IDindex <= 9)
        {
            value = "00" + value + IDindex.ToString();
        }
        else if (IDindex <= 99)
        {
            value = "0" + value + IDindex.ToString();
        }
        else if (IDindex <= 999)
        {
            //value = "00" + value + IDindex.ToString();
            value = value + IDindex.ToString();
        }
        else if (IDindex <= 9999)
        {
            value = "0" + IDindex.ToString();
        }        
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
    }
    return value;
}

Here is how my SQL table:

CREATE TABLE [dbo].[Students] (
    [StudentID]          INT             IDENTITY (1, 1) NOT NULL,
    [StudentNumber]      NVARCHAR (50)   NOT NULL,
);

Thank you very much.

Dale K
  • 25,246
  • 15
  • 42
  • 71
John123
  • 17
  • 6
  • Which dbms are you using? (Perhaps MS SQL Server?) – jarlh May 17 '21 at 09:34
  • 5
    _"but my problem because it taking the last ID from database to generate the student ID"_ the problem is not only that you need to generate it for every year but that your code generates it. This is solely a task for the database which is also responsible for the ID. Otherwise thats a race condition which your app will lose sooner or later. – Tim Schmelter May 17 '21 at 09:37
  • 7
    Most databases, including SQL Server, have a [SEQUENCE](https://learn.microsoft.com/en-us/sql/relational-databases/sequence-numbers/sequence-numbers?view=sql-server-ver15) type that can be used for this. – Panagiotis Kanavos May 17 '21 at 09:38
  • Maybe a [composite primary key](https://stackoverflow.com/questions/3922337/how-to-create-composite-primary-key-in-sql-server-2008/3922359) would be better? –  May 17 '21 at 09:38
  • Compare the current year against the year used in the ID. When the years are different then reset the counter to one. – jdweng May 17 '21 at 09:39
  • 2
    This sounds like data you should compute using row_number and partitioned by year. – Stu May 17 '21 at 09:43
  • 1
    *When* do you reset the sequence ? Based on what criteria? If you use the current time, it's a *lot* easier to use a SEQUENCE, set the default value for `StudentNumber `to `NEXT VALUE FOR StudentCounter` and reset it once a year. Since you're talking about StudentIDs, the "year" won't be a calendar year anyway – Panagiotis Kanavos May 17 '21 at 09:43
  • @Stu academic years don't start on January 1st, nor are they fixed in stone. I bet almost all countries had to change academic years due to the pandemic. – Panagiotis Kanavos May 17 '21 at 09:45
  • You could skip it; just use a normal autoinc int for your PK and generate the ID after with a query like `CONCAT(YEAR(createddate), FORMAT(ROW_NUMBER() OVER(PARTITION BY YEAR(createddate) ORDER BY id, '000000'))`. – Caius Jard May 17 '21 at 09:54
  • 1
    There are 2 parts to the problem, firstly what to store, secondly how to generate it. Assuming a student record contains the date it was created you have the year. If not you have to store either the entire date or the year. Secondly you need to store the sequence within the year. The complete student number can then be a computed column. When I have done this I have found it easier to roll my own sequence table, because you need a sequence per year (in my case I needed a sequence per year, per organisation). And of course this isn't going to be your PK id, use a regular autoinc for that. – Dale K May 17 '21 at 10:00
  • 1
    (The issue I take with manufactured PKs like this is that they often fall back to a SELECT MAX+1 strategy or similar, that causes pain in concurrent situations. I support Panagiotis' suggestion to have a sequence to put the onus on generation back into the DB - it's best to find a strategy where the DB handles the concurrency (and to my mind inserting a record in a normal way with a normal int inc PK, and then later working out what Student ID it should have works because the PK should not alter) – Caius Jard May 17 '21 at 10:03
  • Can you tell us the actual problem you are trying to solve? You already have a natural 'StudentNumber' Key... What's wrong with an identity sequence for 'StudentId'? You are creating aproblem you don't really have... – Mitch Wheat May 17 '21 at 10:04
  • 2
    By the way, your entire if block can be replaced with some tweak of `IDIndex.ToString("00000")`. Do not name local variables using PascalCase; use camelCase – Caius Jard May 17 '21 at 10:05
  • 1
    Actually thinking about it, if you combine my thoughts above with the row_number suggestion (instead of a sequence) from Caius to allocation a new sequence to any record which doesn't have one. Probably in a trigger, unless you can put your "create student" SQL within a stored procedure. – Dale K May 17 '21 at 10:08
  • Thanks all for your comments and suggestions. I am considering all your suggestions and see what will work for me. The solutions should be simple, the sequence does not have to follow a strict rule as Panagiotis Kanavos as suggested, academic year doesn't have to start a certain specific month, the year part is simple to get from the current year, the sequence could be reset in any month, not critical in this application what the exact student ID is, the problem is not to keep incrementing until it reaches a huge number affecting the student ID format, that's why a need to reset. – John123 May 17 '21 at 10:58

1 Answers1

1

My recommendation is to set up a sequence:

 CREATE SEQUENCE StudentSequenceWithinYear;

Once a year, you can run a job to reset the value:

ALTER SEQUENCE StudentSequenceWithinYear RESTART WITH 1 ;  

You can set this up automatically to run around midnight on Dec 31st/Jan 1st.

Then in the CREATE TABLE statement, you can use:

CREATE TABLE Students (
    StudentID  INT IDENTITY (1, 1) NOT NULL,
    StudentNumber NVARCHAR (50) NOT NULL,
    Year INT DEFAULT YEAR(GETDATE()),
    WithinYearSequence INT DEFAULT NEXT VALUE FOR StudentSequenceWithinYear,
    MyStudentId AS (CONCAT(Year, FORMAT(WithinYearSequence, '00000'))) 
);

All that said, I think you should just use an identity column for identifying students. Trying to store information within an id usually causes problems with edge cases (a student who drops out one year but returns another, for instance). You can always look up the year in your database, so you don't need to store that as part of the id.

Gordon Linoff
  • 1,242,037
  • 58
  • 646
  • 786
  • Thanks Gordon. I haven't used sequence before, but this option seems like something that could work in my application. I will study your suggestions. – John123 May 17 '21 at 11:05