1

I am following a tutorial on how to create a user registration system for a website. I've made it to this step but I need to check multiple values in two tables to see if they exist. The tutorial doesn't do this.

Here is my registration form:

<form action="/members/register/" name="registerForm">
    <div>
        <h2>Register</h2>
        <h6>welcome</h6>
        <div class="register">
            <input type="text" name="firstname" placeholder="First Name" required>
            <input type="text" name="lastname" placeholder="Last Name" required>
            <input type="text" name="email" placeholder="Email" required>
            <label for="dob">Date of Birth:
                <input type="date" name="dob" id="dob" placeholder="Date of Birth" required>
            </label>
            <input type="text" name="username" placeholder="Username" required>
            <input type="password" name="password" placeholder="Password" required>
            <input type="password" name="passwordconfirm" placeholder="Confirm Password" required>
            <p>By creating an account you agree to our <a href="/legal/terms-of-use/">Terms &amp; Privacy</a>.</p>
            <button type="submit" name="registerNow">Register</button>
        </div>
    </div>
</form>

The mySQL database tables I need to check against are:

users
  -id
  -username
  -password
  -userID (foreign key)

registered
  -id
  -nameFirst
  -nameLast
  -email
  -dob

I need to create a query that checks if ANY of the following already exist: 1) first AND last name together, 2) username, or 3) email.

Furthermore, once I understand how to perform a query like this, I am still a little confused as to what the ? is in the query. Also, this code example only checks if the username exists and outputs 'Username already exists. Please choose another.' I need to output different things based on which fields already exist in the table(s). Here is the code from the tutorial:

if ($stmt = $con->prepare('SELECT id, password FROM accounts WHERE username = ?')) {
// Bind parameters (s = string, i = int, b = blob, etc), hash the password using the PHP password_hash function.
$stmt->bind_param('s', $_POST['username']);
$stmt->execute();
$stmt->store_result();
// Store the result so we can check if the account exists in the database.
if ($stmt->num_rows > 0) {
    // Username already exists
    echo 'Username already exists. Please choose another.';
} else {
    // Insert new account
}
$stmt->close();
} else {
// Something is wrong with the sql statement, check to make sure accounts table exists with all 3 fields.
echo 'Could not prepare statement!';
}
$con->close();

Would it be like this?

SELECT id, password 
FROM users, registered 
WHERE users.username = ? OR (registered.nameFirst = ? AND registered.nameLast = ?) OR registered.email = ?

And again, I am learning how to do this using the tutorial so I don't want to change anything to the code in terms of how it operates. I understand that there are much better ways of doing this. I am using this as a starting point to learn and progress from.

Angeliss44
  • 125
  • 1
  • 3
  • 20
  • 1
    There shouldn't be two tables in the first place. All your fields must be in a single table – Your Common Sense Nov 06 '19 at 04:53
  • @YourCommonSense The answers below prove that all of the fields need not be in a single table, do they not? I'm not understanding why you would say that all of the fields must be in a single table. If there is a reason that they SHOULD be in a single table rather than MUST be in a single table please let me know. Like I stated above in my question, I am new to this and I am learning so any help would be appreciated. – Angeliss44 Nov 06 '19 at 05:22
  • 1
    Answers below prove nothing, they just answer your question. There is no reason to split a single data entity between several tables in the first place. It is you who must explain the unnatural design. If you are new to this, then ask a question how to design your database before starting your application – Your Common Sense Nov 06 '19 at 05:25
  • @YourCommonSense Thank you for directing me to that. Years ago when I was learning c# and Access I was taught to make tables as light as possible by grouping the data inside by relation. Therefore, I was taught with login systems to keep only username and salt/hash in a table and all other user data in a separate table. This article is very similar to what I was taught and after reading it I feel it has pretty valid points. At the same time, after your last comment as I was searching for the best database layout I found numerous answers in line with your comment. – Angeliss44 Nov 06 '19 at 06:02
  • Therfore, I'm not sure which way I should be creating them as all of the answers that supported your opinion didn't offer any negatives to the multi-table layout and I really like the organization of doing it that way. Here is the article I mentioned above https://www.donedone.com/building-the-optimal-user-database-model-for-your-application/ – Angeliss44 Nov 06 '19 at 06:05
  • 1
    This article has some points, valid or not, for its own task. It is about a multitenant design and your case is not. Better read about KISS principle and only complicate your system for a clearly formulated technical reason valid for your own system, not because someone did something for their entirely different system. When you are trying to learn, you must keep things simpler not more complicated. Your design already made you to ask this question. Your code is already ten times more complex than it should be for a simple registration – Your Common Sense Nov 06 '19 at 06:33
  • I appreciate your response and agree with you. Something I haven't mentioned is that this is for a closed system like the one mentioned in the article. I have a pre-authorized list of people that can access the system and will have a partial record already in the registered table. Upon reading the article though I realized that what I should be doing is checking first if the firstname and lastname are in the table because nothing else matters if they aren't pre-approved to use the system. From there I can check for the other fields if they are in the table. Unless there is a better way? – Angeliss44 Nov 06 '19 at 06:44
  • I don't see any reason why can't you have everything in a single table and check everything in a single query – Your Common Sense Nov 06 '19 at 07:01
  • 1
    Most relational database design is considered correct when it is properly normalized (there are exceptions, but it's like the old saying about needing to know and understand the rules before you break them). Here's a link about the various forms of normalization, and how to achieve them: https://www.guru99.com/database-normalization.html – CXJ Nov 07 '19 at 15:51

2 Answers2

4

Since all you want to do is check whether the registration values already exist, it is probably easiest to use EXISTS subqueries rather than JOINing both tables together. Something like this:

SELECT
    EXISTS (SELECT * FROM users WHERE username = ?) AS found_username,
    EXISTS (SELECT * FROM registered WHERE nameFirst = ? AND nameLast = ?) AS found_name,
    EXISTS (SELECT * FROM registered WHERE email = ?) AS found_email

In this query the ? represent placeholders for the username, first name, last name and email values. The purpose of using prepared statements with placeholders is to protect against SQL injection, for more information on that see this Q&A. Using them also has the benefit of removing the need to escape special character in inputs (for example, if you wanted to insert O'Hara into the nameLast field using a value enclosed in single quotes).

So, for your code, you would do something like:

if ($stmt = $con->prepare('SELECT
        EXISTS (SELECT * FROM users WHERE username = ?) AS found_username,
        EXISTS (SELECT * FROM registered WHERE nameFirst = ? AND nameLast = ?) AS found_name,
        EXISTS (SELECT * FROM registered WHERE email = ?) AS found_email')) {
    // Bind parameters (s = string, i = int, b = blob, etc)
    $stmt->bind_param('ssss', $_POST['username'], $_POST['firstname'], $_POST['lastname'], $_POST['email']);
    $stmt->execute();
    $stmt->bind_result($found_username, $found_name, $found_email);
    $stmt->fetch();
    // Store the result so we can check if the account exists in the database.
    if ($found_username) {
        // Username already exists
        echo 'Username already exists. Please choose another.';
    }
    elseif ($found_name) {
        // Name already exists
        echo 'Name already exists. Please choose another.';
    }
    elseif ($found_email) {
        // Email already exists
        echo 'Email already exists. Please choose another.';
    }
    else {
        // Insert new account
    }
    $stmt->close();
} 
else {
    // Something is wrong with the sql statement, check to make sure accounts table exists with all 3 fields.
    echo 'Could not prepare statement!';
}
$con->close();
Nick
  • 138,499
  • 22
  • 57
  • 95
2

I think you'll want your SQL statement to use a JOIN like this, to avoid duplicate row matching:

    SELECT 
      users.id, password 
    FROM 
      users JOIN registered 
    USING(id) 
    WHERE 
      username = ? 
      OR (nameFirst = ? AND nameLast = ?) 
      OR email = ?

Then you'll need to add the additional parameters to your bind:

$stmt->bind_param(
   'ssss', 
   $_POST["username"], 
   $_POST["firstname"], 
   $_POST["lastname"], 
   $_POST["email]
);
$stmt->execute();
// etc.
CXJ
  • 4,301
  • 3
  • 32
  • 62
  • 2
    There's no guarantee that the `id` values in the `users` table and `registered` table have the same set of values, in which case the `JOIN` will omit some rows from `registered` – Nick Nov 06 '19 at 02:59
  • 1
    You're exactly right. I made an assumption that one was a foreign key for the other table, since the whole idea was to register unique users. But that may well be a faulty assumption on my part. Thanks for pointing that out. – CXJ Nov 06 '19 at 05:12
  • 2
    It's not an unreasonable assumption, good table design would say that there should be a foreign key relationship between `users` and `registered`, but it's not clear that is present in this particular situation. – Nick Nov 06 '19 at 05:25
  • @ Nick you are absolutely right. I originally thought that if data was being inserted into both tables simultaneously that the auto increment id would be the same for both. Realizing that isn't true and that I will already have data in the registration table, I have added a foreign key to the users table. Thank you for pointing this out. – Angeliss44 Nov 06 '19 at 07:56